summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--public/favicon.icobin0 -> 4286 bytes
-rw-r--r--src/README.md2
-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
-rwxr-xr-xsrc/compile-sass.sh9
-rw-r--r--src/db/db.go49
-rw-r--r--src/go.mod27
-rw-r--r--src/go.sum59
-rw-r--r--src/main.go43
-rw-r--r--src/web/styles/home.scss133
-rw-r--r--src/web/styles/partials/_article.scss31
-rw-r--r--src/web/styles/partials/_home.scss27
-rw-r--r--src/web/styles/partials/_navbar.scss35
-rw-r--r--src/web/styles/partials/_tags_menu.scss122
-rw-r--r--src/web/styles/post/post.scss166
-rw-r--r--src/web/styles/post/posts.scss94
-rw-r--r--src/web/styles/styles.scss30
-rw-r--r--src/web/styles/theme/_global.scss39
-rw-r--r--src/web/styles/theme/_home.scss17
-rw-r--r--src/web/styles/theme/_navbar.scss10
-rw-r--r--src/web/styles/theme/_post.scss62
-rw-r--r--src/web/templates/partials/navbar.html9
-rw-r--r--src/web/templates/partials/tags_menu.html18
-rw-r--r--src/web/templates/views/home.html54
-rw-r--r--src/web/templates/views/post.html44
-rw-r--r--src/web/templates/views/posts.html67
29 files changed, 1551 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e4455cc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+public/css
+src/.env
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..c0dad02
--- /dev/null
+++ b/public/favicon.ico
Binary files differ
diff --git a/src/README.md b/src/README.md
new file mode 100644
index 0000000..453487b
--- /dev/null
+++ b/src/README.md
@@ -0,0 +1,2 @@
+# vidhublog
+My Blog
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)
+}
diff --git a/src/compile-sass.sh b/src/compile-sass.sh
new file mode 100755
index 0000000..1d6ff48
--- /dev/null
+++ b/src/compile-sass.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+cmd="sass $@"
+
+$cmd web/styles/styles.scss:../public/css/styles.css &
+$cmd web/styles/home.scss:../public/css/home.css &
+$cmd web/styles/post/posts.scss:../public/css/posts.css &
+$cmd web/styles/post/post.scss:../public/css/post.css &
+
diff --git a/src/db/db.go b/src/db/db.go
new file mode 100644
index 0000000..7ca96ff
--- /dev/null
+++ b/src/db/db.go
@@ -0,0 +1,49 @@
+/*
+ * 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 db
+
+import (
+ "database/sql"
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/joho/godotenv"
+ "os"
+)
+
+// ConnectDB opens a connection to the database
+// TODO: properly handle all errors
+func ConnectDB() *sql.DB {
+ // load ENV
+ err := godotenv.Load()
+ if err != nil {
+ panic(err.Error())
+ }
+
+ // read ENV
+ username := os.Getenv("DB_USERNAME")
+ password := os.Getenv("DB_PASSWORD")
+ database := os.Getenv("DB_DATABASE")
+
+ // connect to database
+ db, err := sql.Open("mysql", username + ":" + password + "@/" + database)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ return db
+}
diff --git a/src/go.mod b/src/go.mod
new file mode 100644
index 0000000..06783e4
--- /dev/null
+++ b/src/go.mod
@@ -0,0 +1,27 @@
+module github.com/MikunoNaka/vidhublog
+
+go 1.18
+
+require (
+ github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19
+ github.com/gin-gonic/gin v1.7.7
+ github.com/go-sql-driver/mysql v1.6.0
+)
+
+require (
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-playground/locales v0.13.0 // indirect
+ github.com/go-playground/universal-translator v0.17.0 // indirect
+ github.com/go-playground/validator/v10 v10.4.1 // indirect
+ github.com/golang/protobuf v1.3.3 // indirect
+ github.com/joho/godotenv v1.4.0 // indirect
+ github.com/json-iterator/go v1.1.9 // indirect
+ github.com/leodido/go-urn v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.12 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
+ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
+ github.com/ugorji/go/codec v1.1.7 // indirect
+ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
+ golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect
+ gopkg.in/yaml.v2 v2.2.8 // indirect
+)
diff --git a/src/go.sum b/src/go.sum
new file mode 100644
index 0000000..0f84be3
--- /dev/null
+++ b/src/go.sum
@@ -0,0 +1,59 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 h1:J2LPEOcQmWaooBnBtUDV9KHFEnP5LYTZY03GiQ0oQBw=
+github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg=
+github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
+github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
+github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/src/main.go b/src/main.go
new file mode 100644
index 0000000..e7d12b9
--- /dev/null
+++ b/src/main.go
@@ -0,0 +1,43 @@
+/*
+ * 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 main
+
+import (
+ "github.com/gin-gonic/contrib/static"
+ "github.com/gin-gonic/gin"
+
+ // internal routers
+ "github.com/MikunoNaka/vidhublog/blog"
+)
+
+func main() {
+ // initialise the router
+ r := gin.New()
+ r.Use(gin.Logger())
+ r.Use(gin.Recovery())
+
+ // in production nginx should handle the static files
+ r.Use(static.Serve("/", static.LocalFile("./../public/", true)))
+
+ r.GET("/x", blog.HomePage)
+
+ blog.Routes(r)
+
+ r.Run(":3000")
+}
diff --git a/src/web/styles/home.scss b/src/web/styles/home.scss
new file mode 100644
index 0000000..c2d6242
--- /dev/null
+++ b/src/web/styles/home.scss
@@ -0,0 +1,133 @@
+@import "theme/global";
+@import "theme/home";
+@import "partials/home";
+
+body { @include home-body; }
+
+.header {
+ text-align: center;
+ #welcome-title {
+ color: $welcomeHeadingFG;
+ }
+ #welcome-subtitle {
+ font-size: 1.1em;
+ color: $welcomeHeadingAltFG;
+ }
+}
+
+.links-container {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ // border: 1px solid pink;
+
+ .links {
+ max-width: 42em;
+ width: 100%;
+
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
+
+ a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.1em;
+ background-color: $buttonBackgroundColor;
+ height: 3rem;
+ min-width: 10em;
+ border: 1px solid $buttonBorderColor;
+ border-radius: 2em;
+ }
+ }
+}
+
+.recents {
+ display: flex;
+ flex-direction: column;
+
+ .recents-label {
+ color: $recentsLabelFG;
+ margin-bottom: 0.5rem;
+ }
+
+ .posts, .tags {
+ background-color: $recentsItemBG;
+ border: 1px solid $buttonBorderColor;
+ width: 100%;
+ padding: 3px 7px;
+ overflow-y: scroll;
+ }
+
+ .posts {
+ font-size: 1.3rem;
+ max-height: 13em;
+ .post {
+ margin: 0.3em auto;
+ a {
+ word-break: break-word;
+ color: $postTitleFG;
+ // font-weight: bold;
+ font-size: 0.95em;
+ }
+ a:hover {
+ color: $defLinkForeground;
+ }
+ .post-createdat {
+ color: $postCreatedAtFG;
+ font-size: 0.9em;
+ }
+ }
+ }
+
+ .tags {
+ height: 28em;
+ }
+}
+
+@media screen and (max-width: 1200px) {
+ body { @include home-body-1200px; }
+}
+
+@media screen and (max-width: 800px) {
+ body { @include home-body-800px; }
+ .links-container .links {
+ a {
+ font-size: 1em;
+ min-width: 9em;
+ margin: 0.3em auto;
+ }
+ }
+}
+
+@media screen and (max-width: 600px) {
+ body { @include home-body-600px; }
+ .links-container .links {
+ a {
+ font-size: 0.9em;
+ min-width: 8.5em;
+ }
+ }
+ .recents {
+ .recents-label {
+ padding: 0 0.8rem
+ }
+ .posts, .tags {
+ padding: 0.2rem 0.8rem;
+ width: 100%;
+ border: none;
+ }
+ .posts {
+ // background-color: $defBackgroundColor;
+ .post {
+ a {
+ font-size: 0.8em;
+ }
+ .post-createdat {
+ font-size: 0.7em;
+ }
+ }
+ }
+ }
+}
diff --git a/src/web/styles/partials/_article.scss b/src/web/styles/partials/_article.scss
new file mode 100644
index 0000000..341ff94
--- /dev/null
+++ b/src/web/styles/partials/_article.scss
@@ -0,0 +1,31 @@
+@import "../theme/post";
+
+@mixin article-styling {
+ background-color: $postDefBackgroundColor;
+ min-width: 1150px;
+ width: 60%;
+ max-width: 1920px;
+ margin: 3em auto;
+ padding: 1em 2em;
+ border: 1px dotted $articleBorderColor;
+}
+
+@mixin article-styling-1200px {
+ margin: 2em auto;
+ min-width: 780px;
+ width: 90%;
+}
+
+@mixin article-styling-800px($val) {
+ min-width: 560px;
+ width: auto;
+ margin: calc($val * 1.5) $val;
+}
+
+@mixin article-styling-600px($val) {
+ min-width: 0;
+ width: 100%;
+ margin: 0;
+ border: none;
+ padding: $val;
+}
diff --git a/src/web/styles/partials/_home.scss b/src/web/styles/partials/_home.scss
new file mode 100644
index 0000000..462b67b
--- /dev/null
+++ b/src/web/styles/partials/_home.scss
@@ -0,0 +1,27 @@
+@import "../theme/global";
+
+@mixin home-body {
+ background-color: $defBackgroundColor;
+ min-width: 1150px;
+ width: 70%;
+ max-width: 1920px;
+ margin: 0em auto;
+ padding: 1em 2em;
+}
+
+@mixin home-body-1200px {
+ margin: 2em auto;
+ min-width: 780px;
+ width: 90%;
+}
+
+@mixin home-body-800px {
+ min-width: 560px;
+}
+
+@mixin home-body-600px {
+ min-width: 0;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+}
diff --git a/src/web/styles/partials/_navbar.scss b/src/web/styles/partials/_navbar.scss
new file mode 100644
index 0000000..0b9c9b1
--- /dev/null
+++ b/src/web/styles/partials/_navbar.scss
@@ -0,0 +1,35 @@
+@import "../theme/global";
+@import "../theme/navbar";
+
+#navbar {
+ height: $navbarHeight;
+ width: 100%;
+ background-color: $navbarBackgroundColor;
+ position: sticky;
+ top: 0;
+ box-shadow: $navbarShadow;
+
+ ul {
+ list-style-type: none;
+ height: 100%;
+ min-width: 400px;
+ max-width: 500px;
+ margin: 0 auto;
+ padding: 0;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+
+ li {
+ padding: 0;
+ margin: 0 0.5em;
+ }
+
+ a {
+ font-size: 1.3em;
+ font-weight: 500;
+ text-decoration: none;
+ color: $navbarForegroundColor;
+ }
+ }
+}
diff --git a/src/web/styles/partials/_tags_menu.scss b/src/web/styles/partials/_tags_menu.scss
new file mode 100644
index 0000000..b74b139
--- /dev/null
+++ b/src/web/styles/partials/_tags_menu.scss
@@ -0,0 +1,122 @@
+@import "../theme/global";
+@import "../theme/post";
+@import "../theme/home";
+
+$tagButtonBG: $altBackgroundColor;
+
+@mixin button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: $tagButtonBG;
+ color: $defForegroundColor;
+ font-size: 1.1em;
+ padding: 0.5em 1.3em;
+ border-radius: 2em;
+ border: 1px solid $buttonBorderColor;
+ transition: background-color 400ms, top ease 400ms;
+ position: relative;
+ top: 0;
+}
+
+@mixin button-hover {
+ transition: background-color 250ms, top ease 250ms;
+ top: -3px;
+}
+
+.tags-menu {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ max-width: 1920px;
+ width: 100%;
+ margin: auto;
+
+ .tags-switcher::before {
+ content: "Filter By Tags: ";
+ font-size: 1.5rem;
+ margin: 0.5rem 0;
+ color: $tagsMenuLabelFG;
+ }
+
+ .tags-switcher {
+ min-width: 1000px;
+ width: 50%;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-around;
+ .options, .menu {
+ margin: 0.5em auto;
+ }
+ .options {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-around;
+ flex-wrap: wrap;
+ min-width: 80%;
+ .tag-label {
+ justify-self: center;
+ margin: 0.1em 1em;
+ .tag {
+ @include button;
+ .active {
+ border: 1px solid green;
+ background-color: blue;
+ }
+ }
+ .tag.active {
+ border-color: $tagButtonActiveBG;
+ }
+ input {
+ display: none;
+ }
+ input:checked + .tag {
+ background-color: $tagButtonActiveBG;
+ @include button-hover;
+ }
+ }
+ }
+ .menu {
+ input {
+ @include button;
+ background-color: rgba($tagButtonActiveBG, 0.9);
+ margin: 0.1em 1em;
+ }
+ input:hover {
+ @include button-hover;
+ background-color: $tagButtonActiveBG;
+ }
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ }
+}
+
+@media screen and (max-width: 1200px) {
+ .tags-menu {
+ min-width: 700px;
+
+ .tags-switcher {
+ min-width: 0;
+ width: 100%;
+ }
+ }
+}
+
+@media screen and (max-width: 800px) {
+ .tags-menu {
+ min-width: 0;
+ .tags-switcher {
+ // for some reason it won't work unless I do .1 .2 .3 thing
+ .options .tag-label .tag,
+ .menu input {
+ font-size: 1em;
+ }
+ }
+ }
+}
diff --git a/src/web/styles/post/post.scss b/src/web/styles/post/post.scss
new file mode 100644
index 0000000..80301d4
--- /dev/null
+++ b/src/web/styles/post/post.scss
@@ -0,0 +1,166 @@
+@import "../theme/global";
+@import "../theme/post";
+@import "../partials/article";
+
+article {
+ @include article-styling;
+ .post-header {
+ .post-title {
+ font-size: $postTitleFontSize;
+ text-align: $postTitleAlignment;
+ color: $postTitleColor;
+ word-break: break-word;
+ }
+ .post-info {
+ color: $postInfoForegroundColor;
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ }
+ }
+
+ .header-seperator {
+ margin: 1.1em 0;
+ }
+
+ img {
+ object-fit: contain;
+ min-width: 300px;
+ max-width: 50%;
+ min-height: 300px;
+ max-height: 600px;
+ height: auto;
+ }
+
+ img.root-img {
+ min-width: 400px;
+ width: 60%;
+ // max-height: 600px;
+ }
+
+ img.centered {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .image-flexbox {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+
+ img {
+ all: initial;
+ max-height: 25em;
+ margin: 0.5em;
+ }
+ }
+
+ li {
+ color: $listItemForeground;
+ }
+
+ .post-content {
+ h1 { color: $fg1; }
+ h2 { color: $fg2; }
+ h3 { color: $fg3; }
+ h4 { color: $fg4; }
+ h5 { color: $fg5; }
+ h6 { color: $fg6; }
+
+ .code-block {
+ color: $codeBlockForegroundColor;
+ padding: $codeBlockPadding;
+ background-color: $codeBlockBackgroundColor;
+ border-radius: $codeBlockBorderRadius;
+ @include selectionColors (
+ $codeBlockHighlightBackground,
+ $codeBlockHighlightForeground
+ );
+
+ em {
+ all: unset;
+ color: $codeCommentForegroundColor;
+ }
+ }
+ }
+}
+
+.centered {
+ text-align: center;
+}
+
+@media screen and (max-width: 1200px) {
+ article {
+ @include article-styling-1200px;
+ .header-seperator { margin: 1em -2em; }
+ .post-header { .post-title { font-size: 2.2em; } }
+ }
+}
+
+@media screen and (max-width: 800px) {
+ article {
+ $side-margin: $post-800px-padding;
+ @include article-styling-800px($side-margin);
+ .post-header { .post-title { font-size: 2em; } }
+ .header-seperator { margin: 1em (-$side-margin * 2); }
+
+ $img-width: 95%;
+ img {
+ min-width: auto;
+ max-width: $img-width;
+ min-height: auto;
+ max-height: auto;
+ }
+
+ .image-flexbox {
+ img {
+ all: initial;
+ max-height: 600px;
+ max-width: $img-width;
+ margin: 0.5em;
+ }
+ }
+
+ ul {
+ width: 100%;
+ li {
+ width: 100%;
+ .image-flexbox {
+ width: $img-width;
+ img {
+ max-width: 100%;
+ }
+ }
+ }
+ }
+ }
+}
+
+@media screen and (max-width: 600px) {
+ article {
+ $padding: $post-600px-padding;
+ @include article-styling-600px($padding);
+
+ img.root-img {
+ min-width: 300px;
+ width: 80%;
+ // max-height: 600px;
+ }
+
+ .header-seperator {
+ margin: 1em (-$padding);
+ }
+ .post-header {
+ .post-title { font-size: 1.5em; }
+ // NOTE: might need to be modified i.e
+ // content goes to next line instead of overflowing
+ .post-info {
+ justify-content: center;
+ }
+ }
+ }
+}
diff --git a/src/web/styles/post/posts.scss b/src/web/styles/post/posts.scss
new file mode 100644
index 0000000..1272d1d
--- /dev/null
+++ b/src/web/styles/post/posts.scss
@@ -0,0 +1,94 @@
+@import "../theme/global";
+@import "../theme/post";
+@import "../partials/article";
+@import "../partials/tags_menu";
+
+article {
+ @include article-styling;
+ .options {
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+
+ .limit-options {
+ a {
+ color: $defForegroundColor;
+ }
+ .active {
+ color: $defLinkForeground;
+ font-weight: 600;
+ }
+ }
+
+ .paging-options {
+ width: 16em;
+ display: flex;
+ justify-content: space-evenly;
+ }
+ }
+
+ #options-footer {
+ justify-content: center;
+ }
+
+ .posts {
+ .post {
+ padding: 0.5em 0;
+ font-size: 1.4em;
+
+ .post-title-tags {
+ .post-title {
+ font-size: 1em;
+ color: $posts_postTitleColor;
+ }
+
+ .post-title:hover {
+ color: $posts_postHighlightedTitleColor;
+ }
+
+ .post-tag {
+ font-size: 0.8em;
+ color: $posts_postTagForegroundColor;
+ }
+
+ .post-tag:hover {
+ color: $defLinkForeground;
+ }
+ }
+
+ .post-info {
+ font-size: 0.8em;
+ .post-createdat {
+ color: $posts_postInfoForegroundColor;
+ }
+ }
+ }
+ }
+}
+
+@media screen and (max-width: 1200px) {
+ article {
+ @include article-styling-1200px;
+ .post {
+ a {
+ font-size: 1.2rem;
+ text-decoration: none;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: 800px) {
+ article {
+ $side-margin: 1em;
+ @include article-styling-800px($side-margin);
+ }
+}
+
+@media screen and (max-width: 600px) {
+ article {
+ $padding: 1em;
+ @include article-styling-600px($padding);
+ }
+}
diff --git a/src/web/styles/styles.scss b/src/web/styles/styles.scss
new file mode 100644
index 0000000..7a1b0c0
--- /dev/null
+++ b/src/web/styles/styles.scss
@@ -0,0 +1,30 @@
+@import "theme/global";
+@import "partials/navbar";
+
+// TODO: If I like this font I'll host it myself
+// @import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500&display=swap');
+
+* {
+ box-sizing: border-box;
+
+ // font-family: 'Lato', sans-serif;
+ font-family: 'Open Sans', sans-serif;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ background-color: $defBackgroundColor;
+ color: $defForegroundColor;
+}
+
+@include selectionColors($selectionBackground, $selectionForeground);
+
+a {
+ color: $defLinkForeground;
+ text-decoration: none;
+}
+
+
+
diff --git a/src/web/styles/theme/_global.scss b/src/web/styles/theme/_global.scss
new file mode 100644
index 0000000..de77f99
--- /dev/null
+++ b/src/web/styles/theme/_global.scss
@@ -0,0 +1,39 @@
+// global color definitions
+// background
+// $defBackgroundColor: #061820;
+$defBackgroundColor: #151718;
+// $altBackgroundColor: #1a2a30;
+// $defBackgroundColor: #383A59;
+// $altBackgroundColor: #282A36;
+$altBackgroundColor: #232627;
+$selectionBackground: #8215D2;
+
+// foreground
+$defForegroundColor: #dfdfdf;
+$altForegroundColor: #FFFFFF;
+$boringForeground: #b5c4c2;
+$selectionForeground: #FFFFFF;
+// $defLinkForeground: #3691b8;
+$defLinkForeground: #6d8ee2;
+
+// colored foregrounds
+$fg-purple0: #9c79c5;
+
+$fg-pink0: #e82884;
+
+$fg-green0: #9c79c5;
+
+$fg-yellow0: #FFFF99;
+
+
+// apply text selection/highlight colors easily
+@mixin selectionColors($background, $foreground) {
+ ::selection {
+ color: $foreground;
+ background-color: $background;
+ }
+ ::-moz-selection {
+ color: $foreground;
+ background-color: $background;
+ }
+}
diff --git a/src/web/styles/theme/_home.scss b/src/web/styles/theme/_home.scss
new file mode 100644
index 0000000..037c2d3
--- /dev/null
+++ b/src/web/styles/theme/_home.scss
@@ -0,0 +1,17 @@
+@import "global";
+@import "post";
+
+$buttonBackgroundColor: $altBackgroundColor;
+
+$buttonBorderColor: #000000;
+
+$welcomeHeadingFG: $fg-purple0;
+$welcomeHeadingAltFG: $fg-yellow0;
+$recentsLabelFG: $fg-pink0;
+
+$recentsItemBG: $altBackgroundColor;
+
+// $postTitleFG: #d28bf0;
+$postTitleFG: $defForegroundColor;
+// $postCreatedAtFG: #959495;
+$postCreatedAtFG: #c2c1c2;
diff --git a/src/web/styles/theme/_navbar.scss b/src/web/styles/theme/_navbar.scss
new file mode 100644
index 0000000..1f01882
--- /dev/null
+++ b/src/web/styles/theme/_navbar.scss
@@ -0,0 +1,10 @@
+// color definitions
+$navbarBackgroundColor: $defBackgroundColor;
+// $navbarForegroundColor: #bbf2c0;
+$navbarForegroundColor: #EDEDED;
+
+// $navbarShadow: 0 0.5em 1em $navbarBackgroundColor;
+$navbarShadow: none;
+
+// size definitions
+$navbarHeight: 3rem;
diff --git a/src/web/styles/theme/_post.scss b/src/web/styles/theme/_post.scss
new file mode 100644
index 0000000..3a05a74
--- /dev/null
+++ b/src/web/styles/theme/_post.scss
@@ -0,0 +1,62 @@
+@import "global";
+
+// color definitions
+// background
+$postDefBackgroundColor: $altBackgroundColor;
+$postAltBackgroundColor: #384044;
+$codeBlockBackgroundColor: $postAltBackgroundColor;
+$codeBlockHighlightBackground: pink;
+
+// foreground
+$postDefForegroundColor: $defForegroundColor;
+$postAltForegroundColor: $altForegroundColor;
+$postInfoForegroundColor: $boringForeground;
+$codeBlockForegroundColor: $postAltForegroundColor;
+$codeCommentForegroundColor: gray;
+$codeBlockHighlightForeground: $altBackgroundColor;
+
+// $fg1: #d305e8;
+// $fg1: #AB47BC;
+// $fg2: #28c7d4;
+// $fg3: #F26413;
+// $fg4: #23DEBA;
+// $fg5: #A600FF;
+// $fg6: #B623DE;
+
+$fg1: #FF79C6;
+$fg2: #50FA7B;
+$fg3: #E659A9;
+$fg4: #8E5EBC;
+$fg5: #23DEBA;
+$fg6: #A600FF;
+
+$listItemForeground: $fg3;
+
+// other
+$articleBorderColor: $defForegroundColor;
+
+// $postTitleColor: #2aa386;
+// $postTitleColor: #dd237d;
+// $postTitleColor: #ffbb00;
+// $postTitleColor: #dea2c2;
+// $postTitleColor: #BD93F9;
+$postTitleColor: $fg-purple0;
+
+// size definitions
+$postTitleFontSize: 2.5em;
+$postTitleAlignment: center;
+
+$codeBlockBorderRadius: 0.4em;
+$codeBlockPadding: 0.2em 1em;
+
+// responsive
+$post-800px-padding: 1em;
+$post-600px-padding: 1em;
+
+$posts_postTitleColor: $defForegroundColor;
+$posts_postHighlightedTitleColor: $defLinkForeground;
+$posts_postInfoForegroundColor: $postInfoForegroundColor;
+$posts_postTagForegroundColor: $postInfoForegroundColor;
+
+$tagsMenuLabelFG: $postTitleColor;
+$tagButtonActiveBG: $tagsMenuLabelFG;
diff --git a/src/web/templates/partials/navbar.html b/src/web/templates/partials/navbar.html
new file mode 100644
index 0000000..223c3f3
--- /dev/null
+++ b/src/web/templates/partials/navbar.html
@@ -0,0 +1,9 @@
+{{ define "partials/navbar.html" }}
+<nav id="navbar">
+ <ul>
+ <a href="https://www.youtube.com/watch?v=DaWbq6KeJq4&list=RDxA_4ZFe1TME&index=16">
+ <li>This UGLY Navbar Is Just A Placeholder</li>
+ </a>
+ </ul>
+</nav>
+{{ end }}
diff --git a/src/web/templates/partials/tags_menu.html b/src/web/templates/partials/tags_menu.html
new file mode 100644
index 0000000..8f1c3ee
--- /dev/null
+++ b/src/web/templates/partials/tags_menu.html
@@ -0,0 +1,18 @@
+{{ define "partials/tags_menu.html" }}
+<div class="tags-menu">
+ <form class="tags-switcher" action="/posts/filter-by-tags" method="POST">
+ <div class="options">
+ {{ range .Tags }}
+ <label class="tag-label">
+ <input type="checkbox" name="tags" value="{{ .TagID }}" id="{{ .TagID }}" class="{{ if .IsSelected }}active{{ end }}">
+ <span class="tag {{ if .IsSelected }}active{{ end }}">{{ .TagName }}</span>
+ </label>
+ {{ end }}
+ </div>
+ <div class="menu">
+ <input type="reset" value="Clear"/>
+ <input type="submit" value="Filter"/>
+ </div>
+ </form>
+</div>
+{{ end }}
diff --git a/src/web/templates/views/home.html b/src/web/templates/views/home.html
new file mode 100644
index 0000000..8ab791b
--- /dev/null
+++ b/src/web/templates/views/home.html
@@ -0,0 +1,54 @@
+{{ define "views/home.html" }}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="description" content="Vidhu Kant's Blog" />
+ <link type="text/css" rel="stylesheet" href="/css/styles.css">
+ <link type="text/css" rel="stylesheet" href="/css/home.css">
+ <title>Vidhu Kant's Blog</title>
+</head>
+<body>
+ <div class="header" id="header">
+ <h1 id="welcome-title">Welcome to Vidhu Kant's Blog!</h1>
+ <p id="welcome-subtitle">Here, I talk about the various topics that interest me.</p>
+ <p>-- important links, maybe navbar, info (probably shit like last update date or number of blogs this week/month/whatever) --</p>
+ </div>
+
+ <div class="links-container" id="links">
+ <span class="links">
+ <a href="/posts">View All Posts</a>
+ <a href="/tags">Filter By Tags</a>
+ <a href="/about">About This Blog</a>
+ </span>
+ </div>
+
+ <div class="recents" id="recents">
+ <h2 class="recents-label" id="recent-posts-label">
+ My Recent Articles:
+ </h2>
+ <div class="posts" id="recent-posts">
+ {{ range .RecentPosts }}
+ <div class="post">
+ <span class="post-createdat">{{ .CreatedAt }}: </span>
+ <a href="/posts/{{ .ID }}">{{ .Title }}</a>
+ </div>
+ {{ end }}
+ </div>
+ <!-- obviously there are no "recent" tags,
+ just keeping up with naming scheme -->
+ <h2 class="recents-label" id="recent-tags-label">
+ Browse Articles by Tags:
+ </h2>
+ <div class="tags" id="recent-tags">
+ {{ range .Tags}}
+ <a href="/posts/?tags={{ .ID }}">{{ .Name }}</a>
+ {{ end }}
+ </div>
+ </div>
+
+ {{ .Message }}
+</body>
+</html>
+{{ end }}
diff --git a/src/web/templates/views/post.html b/src/web/templates/views/post.html
new file mode 100644
index 0000000..ecd1a14
--- /dev/null
+++ b/src/web/templates/views/post.html
@@ -0,0 +1,44 @@
+{{ define "views/post.html" }}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="description" content="Vidhu Kant's Blog Post #{{ .ID }}: {{ .Title }}" />
+ <link type="text/css" rel="stylesheet" href="/css/styles.css">
+ <link type="text/css" rel="stylesheet" href="/css/post.css">
+ <title>
+ {{ .Title }} | Vidhu Kant's Blog Post
+ </title>
+</head>
+<body>
+ {{ template "partials/navbar.html" .}}
+ <article>
+ <div class="post-header" id="header">
+ <h1 class="post-title" id="title">
+ {{ .Title }}
+ </h1>
+ <div class="post-info" id="info">
+ <span class="post-created-at" id="created-at">
+ {{ .CreatedAt }}
+ {{ if .UpdatedAt }} <span class="post-updated-at" id="created-at">(Last Updated {{ .UpdatedAt }})</span> {{- end }}
+ </span>
+ </div>
+ </div>
+ <hr class="header-seperator">
+
+ <div class="post-content" id="content">
+ {{ .Content }}
+ </div>
+
+ <div class="post-footer" id="footer">
+ {{ .post_footer }}
+ </div>
+ </article>
+ <noscript>
+ <p>Ahh I see you're a man of culture!</p>
+ <p>(Because you have JavaScript disabled)</p>
+ </noscript>
+</body>
+</html>
+{{ end }}
diff --git a/src/web/templates/views/posts.html b/src/web/templates/views/posts.html
new file mode 100644
index 0000000..e992564
--- /dev/null
+++ b/src/web/templates/views/posts.html
@@ -0,0 +1,67 @@
+{{ define "views/posts.html" }}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="description" content="BLOG DESCRIPTION GHERE" />
+ <link type="text/css" rel="stylesheet" href="/css/styles.css">
+ <link type="text/css" rel="stylesheet" href="/css/posts.css">
+ <title>
+ Index Page
+ </title>
+</head>
+<body>
+ {{ template "partials/navbar.html" .}}
+ {{ template "partials/tags_menu.html" .}}
+ <article>
+ <div class="options" id="options-header">
+ <span class="limit-options">
+ Posts to show per page:
+ {{ range .LimitOptions }}
+ <a href="?page={{ $.PageNumber }}&limit={{ . }}&first={{ $.FirstPost }}&sort_by={{ if $.SortByOldest }}oldest{{ else }}newest{{ end }}"
+ class="{{ if eq . $.Limit }}active{{- end }}">
+ {{ . }}
+ </a>
+ {{ end }}
+ </span>
+ <span id="page-sort">
+ Currently sorting by {{ if .SortByOldest }}oldest{{ else }}newest{{ end }} first.
+ <!--a href="?page={{ .PageNumber }}&limit={{ .Limit }}&first={{ .FirstPost }}&sort_by={{ if .SortByOldest }}newest{{ else }}oldest{{ end }}"-->
+ <a href="?limit={{ .Limit }}&sort_by={{ if .SortByOldest }}newest{{ else }}oldest{{ end }}">
+ Sort by {{ if .SortByOldest }}newest{{ else }}oldest{{ end }} first?
+ </a>
+ </span>
+ </div>
+ <div class="posts">
+ {{ range .Posts }}
+ <div class="post">
+ <span class="post-title-tags">
+ <a class="post-title" href="{{ .ID }}">{{ .Title }}</a>
+ <span class="post-tags">
+ {{ range .Tags }}
+ <a class="post-tag" href="/posts?tags={{ .ID }}">{{ .Name }}</a>
+ {{ end }}
+ </span>
+ </span>
+ <div class="post-info">
+ <span class="post-createdat">{{ .CreatedAt }}</span>
+ </div>
+ </div>
+ {{ end }}
+ </div>
+ <div class="options" id="options-footer">
+ <span class="paging-options">
+ {{ if .ShowPrev }}
+ <a href="?page={{ .PrevPage }}&limit={{ .Limit }}&first={{ .PrevFirst }}&sort_by={{ if .SortByOldest }}oldest{{ else }}newest{{ end }}">Previous Page</a>
+ {{- end }}
+
+ {{ if .ShowNext }}
+ <a href="?page={{ .NextPage }}&limit={{ .Limit }}&first={{ .NextFirst }}&sort_by={{ if .SortByOldest }}oldest{{ else }}newest{{ end }}">Next Page</a>
+ {{- end }}
+ </span>
+ </div>
+ </article>
+</body>
+</html>
+{{ end }}