diff options
Diffstat (limited to 'src/blog')
-rw-r--r-- | src/blog/controller.go | 164 | ||||
-rw-r--r-- | src/blog/handler.go | 31 | ||||
-rw-r--r-- | src/blog/routes.go | 167 | ||||
-rw-r--r-- | src/blog/server.go | 40 |
4 files changed, 402 insertions, 0 deletions
diff --git a/src/blog/controller.go b/src/blog/controller.go new file mode 100644 index 0000000..8355e87 --- /dev/null +++ b/src/blog/controller.go @@ -0,0 +1,164 @@ +/* + * vidhublog: Vidhu Kant's Blog + * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@protonmail.ch> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package blog + +import ( + "strconv" + "fmt" + + _ "github.com/go-sql-driver/mysql" +) + +type Post struct { + ID int + CreatedAt string + UpdatedAt *string + Title string + Content string + Tags []Tag +} + +type Tag struct { + ID int + Name string +} + +func (db *dbhandler) getPostCount(tag *int) int { + var query string + if tag != nil { + query = "SELECT COUNT(DISTINCT(PostID)) FROM Post_Tags WHERE TagID = " + strconv.Itoa(*tag) + } else { + // because some posts might not even have tags + query = "SELECT COUNT(*) FROM Posts" + } + + rows, err := db.connection.Prepare(query) + if err != nil { + panic(err.Error()) + } + defer rows.Close() + + var count int + if err := rows.QueryRow().Scan(&count); err != nil { + panic(err) + } + + return count +} + +// start = read from nth row, limit = read n rows +func (db *dbhandler) getPosts(start, limit int, reversed bool, tags string) []Post { + var qry string + if len(tags) < 1 { + qry = "SELECT ID, DATE_FORMAT(CreatedAt, '%D %M %Y'), Title FROM Posts" + } else { + qry = `SELECT DISTINCT Posts.ID, DATE_FORMAT(Posts.CreatedAt, '%D %M %Y'), Posts.Title + FROM Post_Tags + INNER JOIN Posts ON Post_Tags.PostID = Posts.ID + WHERE Post_Tags.TagID IN (` + tags + `)` + } + if reversed { + qry = qry + " ORDER BY ID DESC" + } + qry = fmt.Sprintf("%s LIMIT %d,%d", qry, start, limit) + + rows, err := db.connection.Query(qry) + if err != nil { + panic(err) + } + defer rows.Close() + + var posts []Post + for rows.Next() { + var p Post + err := rows.Scan(&p.ID, &p.CreatedAt, &p.Title) + if err != nil { + panic(err) + } + // load post's tags + p.Tags = db.getPostTags(p.ID) + posts = append(posts, p) + } + + return posts +} + +func (db *dbhandler) getPost(id int) Post { + rows, err := db.connection.Prepare( + `SELECT ID, DATE_FORMAT(CreatedAt, "%D %M %Y"), DATE_FORMAT(UpdatedAt, "%D %M %Y"), Title, Content FROM Posts WHERE ID = ?`, + ) + if err != nil { + panic(err.Error()) + } + defer rows.Close() + + var post Post + if err := rows.QueryRow(id).Scan(&post.ID, &post.CreatedAt, &post.UpdatedAt, &post.Title, &post.Content); err != nil { + // TODO: handle error when rows are empty + post.Content = "404" + // panic(err) + } + + return post +} + +func (db *dbhandler) getPostTags(id int) []Tag { + rows, err := db.connection.Query( + `SELECT Tags.ID, Tags.Name FROM Post_Tags + INNER JOIN Tags ON Post_Tags.TagID = Tags.ID + WHERE Post_Tags.PostID = ` + strconv.Itoa(id), + ) + if err != nil { + panic(err.Error()) + } + defer rows.Close() + + var tags []Tag + for rows.Next() { + var tag Tag + err := rows.Scan(&tag.ID, &tag.Name) + if err != nil { + panic(err) + } + tags = append(tags, tag) + } + + return tags +} + +// returns all tags +func (db *dbhandler) getTags() []Tag { + rows, err := db.connection.Query("SELECT ID, Name FROM Tags") + if err != nil { + panic(err.Error()) + } + defer rows.Close() + + var tags []Tag + for rows.Next() { + var t Tag + err := rows.Scan(&t.ID, &t.Name) + if err != nil { + panic(err) + } + tags = append(tags, t) + } + + return tags +} diff --git a/src/blog/handler.go b/src/blog/handler.go new file mode 100644 index 0000000..d97b24d --- /dev/null +++ b/src/blog/handler.go @@ -0,0 +1,31 @@ +/* + * vidhublog: Vidhu Kant's Blog + * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@protonmail.ch> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package blog + +import ( + "database/sql" +) + +type dbhandler struct { + connection *sql.DB +} + +func newHandler(db *sql.DB) *dbhandler { + return &dbhandler{ connection: db } +} diff --git a/src/blog/routes.go b/src/blog/routes.go new file mode 100644 index 0000000..c16f046 --- /dev/null +++ b/src/blog/routes.go @@ -0,0 +1,167 @@ +/* + * vidhublog: Vidhu Kant's Blog + * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@protonmail.ch> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package blog + +import ( + "html/template" + "net/http" + "strconv" + "strings" + + "github.com/MikunoNaka/vidhukant.xyz/db" + "github.com/gin-gonic/gin" +) + +type tagsSelection struct { + TagID int + TagName string + IsSelected bool +} + +// database connection +var base *dbhandler +func init() { + connection := db.ConnectDB() + base = newHandler(connection) +} + +// receives tags through form POST and redirects to /posts?tags=.... +func filterByTagInput(ctx *gin.Context) { + var tagInput struct { + Tags []int `form:"tags"` + } + ctx.ShouldBind(&tagInput) + tags := tagInput.Tags + + var tagsStringified string + for i, j := range tags { + tagsStringified = tagsStringified + strconv.Itoa(j) + if i != len(tags) - 1 { + tagsStringified = tagsStringified + "," + } + } + + ctx.Redirect(http.StatusMovedPermanently, "/posts?tags=" + tagsStringified) +} + +func getPosts(ctx *gin.Context) { + limitOptions := []int{10, 20, 30} + limit := 10 + // if limit is in url query use that + if l := ctx.Query("limit"); l != "" { + limit, _ = strconv.Atoi(l) + } + + pageNum := 1 + // if pageNum is in url query use that + if p := ctx.Query("page"); p != "" { + pageNum, _ = strconv.Atoi(p) + // pageNum can't be less than 1 + if pageNum < 1 { pageNum = 1 } + } + + // if firstPost is in url query use that + firstPost := limit * (pageNum - 1) + if f := ctx.Query("first"); f != "" { + firstPost, _ = strconv.Atoi(f) + // firstPost can't be less than 0 + if firstPost < 0 {firstPost = 0} + } + + tags := ctx.Query("tags") + + // check and sort by oldest/newest first with oldest as fallback + sortByOldest := false + if s := ctx.Query("sort_by"); s == "oldest" { + sortByOldest = true + } + + // get posts from database + posts := base.getPosts(firstPost, limit, sortByOldest, tags) + + showNext := true + // TODO: if tags are specified, replace with nil + // check if difference between all post count and posts shown is same + if base.getPostCount(nil) - (firstPost + len(posts)) < 1 { + showNext = false + } + + // turn the tags from URL query into []int + var tagsSlice []int + for _, i := range strings.Split(tags, ",") { + t, _ := strconv.Atoi(i) + tagsSlice = append(tagsSlice, t) + } + + // check if particular tags are selected + var selectedTags []tagsSelection + for _, i := range base.getTags() { + var t tagsSelection + t.TagID = i.ID + t.TagName = i.Name + + // check and set t.IsSelected to true + for _, j := range tagsSlice { + if t.TagID == j { + t.IsSelected = true + break + } + } + + selectedTags = append(selectedTags, t) + } + + ctx.HTML(http.StatusOK, "views/posts.html", gin.H { + "LimitOptions": limitOptions, + "Limit": limit, + "FirstPost": firstPost, + "PageNumber": pageNum, + "PrevPage": pageNum - 1, + "ShowPrev": !(firstPost == 0), + "ShowNext": showNext, + "NextPage": pageNum + 1, + "PrevFirst": firstPost - limit, + "NextFirst": firstPost + limit, + "Posts": posts, + "SortByOldest": sortByOldest, + "Tags": selectedTags, + }) +} + +func getPost(ctx *gin.Context) { + id, _ := strconv.Atoi(ctx.Param("id")) + post := base.getPost(id) + + ctx.HTML(http.StatusOK, "views/post.html", gin.H { + "Title": post.Title, + "CreatedAt": post.CreatedAt, + "UpdatedAt": post.UpdatedAt, + "Content": template.HTML(post.Content), + }) +} + +func HomePage(ctx *gin.Context) { + recentPosts := base.getPosts(0, 10, true, "") + tags := base.getTags() + + ctx.HTML(http.StatusOK, "views/home.html", gin.H { + "RecentPosts": recentPosts, + "Tags": tags, + }) +} diff --git a/src/blog/server.go b/src/blog/server.go new file mode 100644 index 0000000..3b1fc17 --- /dev/null +++ b/src/blog/server.go @@ -0,0 +1,40 @@ +/* + * vidhublog: Vidhu Kant's Blog + * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@protonmail.ch> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package blog + +import ( + "github.com/gin-gonic/gin" +) + +func Routes(r *gin.Engine) { + // load html templates + r.LoadHTMLGlob("web/templates/**/*") + + // blog.vidhukant.xyz uses /blog as root + blog := r.Group("/") + + posts := blog.Group("/posts") + + // fetch index page + posts.GET("/", getPosts) + posts.POST("/filter-by-tags", filterByTagInput) + + // fetch a post + posts.GET("/:id", getPost) +} |