summaryrefslogtreecommitdiff
path: root/src/blog
diff options
context:
space:
mode:
Diffstat (limited to 'src/blog')
-rw-r--r--src/blog/controller.go164
-rw-r--r--src/blog/handler.go31
-rw-r--r--src/blog/routes.go167
-rw-r--r--src/blog/server.go40
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)
+}