diff options
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 Binary files differnew file mode 100644 index 0000000..c0dad02 --- /dev/null +++ b/public/favicon.ico 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 }} |