From d450fba0e13436d25f1206a698b4dc3a65c26a94 Mon Sep 17 00:00:00 2001 From: Jonni Liljamo Date: Mon, 28 Aug 2023 14:49:32 +0300 Subject: [PATCH] feat: tag management thingy --- api/tags.go | 67 +++++++++++++++++ db/migrations.go | 6 ++ handlers/tags.go | 62 ++++++++++++++++ static/styles.css | 139 ++++++++++++++++++++++++++++++----- tailwind.config.js | 32 ++++++++ template/templates/tags.tmpl | 55 ++++++++++++++ tixe.go | 3 + 7 files changed, 344 insertions(+), 20 deletions(-) create mode 100644 api/tags.go create mode 100644 handlers/tags.go create mode 100644 template/templates/tags.tmpl diff --git a/api/tags.go b/api/tags.go new file mode 100644 index 0000000..f9db49d --- /dev/null +++ b/api/tags.go @@ -0,0 +1,67 @@ +package api + +import ( + "context" + "log" + "net/http" + "tixe/db" + "tixe/types" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/oklog/ulid/v2" +) + +type postTagsNew struct { + Tag string `form:"tag"` +} + +func TagsNew(c *gin.Context) { + data := &postTagsNew{} + if err := c.Bind(data); err != nil { + log.Printf("[tixe/api] ERROR: Could not bind new tag data: %v", err) + c.String(http.StatusBadRequest, "Could not bind new tag data") + return; + } + + session := sessions.Default(c) + user := session.Get("user").(types.User) + + tagId := ulid.Make().String() + + _, err := db.PgPool.Exec(context.Background(), + "INSERT INTO tags(id, user_id, tag) VALUES($1, $2, $3)", tagId, user.Id, data.Tag) + if err != nil { + log.Printf("[tixe/api] ERROR: Could not create new tag entry in database: %v", err) + c.String(http.StatusInternalServerError, "Could not create new tag entry in database!") + return; + } + + c.Redirect(http.StatusFound, "/tags") +} + +type postTagsDelete struct { + Id string `form:"id"` +} + +func TagsDelete(c *gin.Context) { + data := &postTagsDelete{} + if err := c.Bind(data); err != nil { + log.Printf("[tixe/api] ERROR: Could not bind new tag data: %v", err) + c.String(http.StatusBadRequest, "Could not bind new tag data") + return; + } + + session := sessions.Default(c) + user := session.Get("user").(types.User) + + _, err := db.PgPool.Exec(context.Background(), + "DELETE FROM tags WHERE id = $1 AND user_id = $2", data.Id, user.Id) + if err != nil { + log.Printf("[tixe/api] ERROR: Could not create new tag entry in database: %v", err) + c.String(http.StatusInternalServerError, "Could not create new tag entry in database!") + return; + } + + c.Redirect(http.StatusFound, "/tags") +} diff --git a/db/migrations.go b/db/migrations.go index ecfb028..6216c51 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -69,5 +69,11 @@ func migrations() []string { display_name TEXT NOT NULL, oidc_subject TEXT )`), + fmt.Sprintf(`CREATE TABLE tags ( + id CHAR(26) NOT NULL PRIMARY KEY, + user_id CHAR(26) NOT NULL REFERENCES users(id), + tag TEXT NOT NULL, + UNIQUE (user_id, tag) + )`), } } diff --git a/handlers/tags.go b/handlers/tags.go new file mode 100644 index 0000000..96c13e2 --- /dev/null +++ b/handlers/tags.go @@ -0,0 +1,62 @@ +package handlers + +import ( + "context" + "log" + "net/http" + "tixe/db" + "tixe/template" + "tixe/types" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +type tag struct { + Id string + Tag string + TimesUsed uint +} + +type tagsData struct { + // NOTE: It's this way because it needs to be sorted alphabetically + // Maybe there's some more efficient way to do this, but this is good enough + // for now + Tags []tag +} + +func Tags(c *gin.Context) { + session := sessions.Default(c) + user := session.Get("user").(types.User) + + var tags []tag + // Query tags which the user has defined + // TODO: Query also the amount of times the tag is used, meaning join + // with the links table too. + rows, _ := db.PgPool.Query(context.Background(), + `SELECT id, tag FROM tags + WHERE user_id = $1`, user.Id) + for rows.Next() { + var id string + var tagstr string + err := rows.Scan(&id, &tagstr) + if err != nil { + // FIXME: user doesn't currently know if this happens (though it shouldn't (tm)) + // something something handle me better + log.Printf("[tixe/handlers] ERROR: Failed to scan a row when querying for tags: %v", err) + continue + } + // FIXME: There has to be a better way to do this, right? + // Like, not appending to an array, but some other other data structure + // completely, or somehow we should get the amount of rows first, so + // we can set the array values, instead of doing this append... + tags = append(tags, tag { Id: id, Tag: tagstr, TimesUsed: 0}) + } + + data := tagsData { + Tags: tags, + } + + html := template.TmplEngine.Render("tags.tmpl", map[string]interface{}{"title": "tags", "user": user, "data": data}) + c.Data(http.StatusOK, "text/html", html) +} diff --git a/static/styles.css b/static/styles.css index b5e279c..3f7ecbd 100644 --- a/static/styles.css +++ b/static/styles.css @@ -546,18 +546,51 @@ video { z-index: 50; } +.ml-2 { + margin-left: 0.5rem; +} + +.mr-8 { + margin-right: 2rem; +} + .flex { display: flex; } +.grid { + display: grid; +} + +.hidden { + display: none; +} + .h-4 { height: 1rem; } +.h-5 { + height: 1.25rem; +} + +.h-8 { + height: 2rem; +} + +.h-fit { + height: -moz-fit-content; + height: fit-content; +} + .w-4 { width: 1rem; } +.w-5 { + width: 1.25rem; +} + .w-fit { width: -moz-fit-content; width: fit-content; @@ -593,6 +626,10 @@ video { align-items: center; } +.gap-1 { + gap: 0.25rem; +} + .gap-2 { gap: 0.5rem; } @@ -648,14 +685,14 @@ video { --tw-gradient-stops: var(--tw-gradient-from), #52525b var(--tw-gradient-via-position), var(--tw-gradient-to); } -.to-rose-500 { - --tw-gradient-to: #f43f5e var(--tw-gradient-to-position); -} - .to-amber-500 { --tw-gradient-to: #f59e0b var(--tw-gradient-to-position); } +.to-rose-500 { + --tw-gradient-to: #f43f5e var(--tw-gradient-to-position); +} + .bg-size-200 { background-size: 200% 200%; } @@ -673,6 +710,14 @@ video { fill: #fef08a; } +.stroke-emerald-600 { + stroke: #059669; +} + +.stroke-rose-600 { + stroke: #e11d48; +} + .stroke-yellow-400 { stroke: #facc15; } @@ -689,6 +734,16 @@ video { padding: 1rem; } +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + .pr-4 { padding-right: 1rem; } @@ -712,10 +767,6 @@ video { font-weight: 700; } -.font-medium { - font-weight: 500; -} - .text-blue-500 { --tw-text-opacity: 1; color: rgb(59 130 246 / var(--tw-text-opacity)); @@ -760,20 +811,14 @@ video { transition-duration: 500ms; } -@keyframes bounce { - 0%, 100% { - transform: translateY(-25%); - animation-timing-function: cubic-bezier(0.8,0,1,1); - } - - 50% { - transform: none; - animation-timing-function: cubic-bezier(0,0,0.2,1); - } +.odd\:bg-slate-200:nth-child(odd) { + --tw-bg-opacity: 1; + background-color: rgb(226 232 240 / var(--tw-bg-opacity)); } -.hover\:animate-bounce:hover { - animation: bounce 1s infinite; +.even\:bg-slate-100:nth-child(even) { + --tw-bg-opacity: 1; + background-color: rgb(241 245 249 / var(--tw-bg-opacity)); } .hover\:bg-slate-200:hover { @@ -798,6 +843,56 @@ video { visibility: visible; } +@keyframes fullrotate { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.group:hover .group-hover\:animate-\[fullrotate_500ms_ease-in-out\] { + animation: fullrotate 500ms ease-in-out; +} + +@keyframes lidflip { + 0% { + transform: rotate(0deg); + } + + 50% { + transform: rotate(180deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.group:hover .group-hover\:animate-lidflip { + animation: lidflip 500ms linear; +} + +@keyframes lidjump { + 0% { + transform: translateY(0); + } + + 50% { + transform: translateY(-75%); + } + + 100% { + transform: translateY(0); + } +} + +.group:hover .group-hover\:animate-lidjump { + animation: lidjump 500ms linear; +} + @keyframes starmove { 0% { transform: translateX(-25%) translateY(-250%); @@ -880,3 +975,7 @@ video { .group:hover .group-hover\:animate-wiggle { animation: wiggle 200ms ease-in-out; } + +.group:hover .group-hover\:stroke-emerald-700 { + stroke: #047857; +} diff --git a/tailwind.config.js b/tailwind.config.js index 159145d..3c21bc5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -63,11 +63,43 @@ module.exports = { transform: 'translateX(-10%) translateY(75%)' }, }, + lidjump: { + '0%': { + transform: 'translateY(0)', + }, + '50%': { + transform: 'translateY(-75%)', + }, + '100%': { + transform: 'translateY(0)', + }, + }, + lidflip: { + '0%': { + transform: 'rotate(0deg)', + }, + '50%': { + transform: 'rotate(180deg)', + }, + '100%': { + transform: 'rotate(360deg)', + }, + }, + fullrotate: { + '0%': { + transform: 'rotate(0deg)', + }, + '100%': { + transform: 'rotate(360deg)', + }, + }, }, animation: { wiggle: 'wiggle 200ms ease-in-out', starspin: 'starspin 750ms linear', starmove: 'starmove 750ms linear', + lidjump: 'lidjump 500ms linear', + lidflip: 'lidflip 500ms linear', }, }, }, diff --git a/template/templates/tags.tmpl b/template/templates/tags.tmpl new file mode 100644 index 0000000..5a92703 --- /dev/null +++ b/template/templates/tags.tmpl @@ -0,0 +1,55 @@ +{{ define "content" }} +

+ Tags +

+
+

New Tag

+
+ + +
+
+
+
+

Name

+

Uses

+
+ {{ range $tag := .data.Tags }} +
+

{{ $tag.Tag }}

+
+

{{ $tag.TimesUsed }}

+
+
+ + +
+
+ {{ end }} +
+{{ end }} diff --git a/tixe.go b/tixe.go index 504f936..7d695da 100644 --- a/tixe.go +++ b/tixe.go @@ -64,12 +64,15 @@ func setupRouter(auth *auth.Auth) *gin.Engine { { reqAuth.GET("/", root) + reqAuth.GET("/tags", handlers.Tags) reqAuth.GET("/settings", handlers.Settings) apiRoute := reqAuth.Group("/api") { apiRoute.GET("/", api.Root) apiRoute.GET("/ping", ping) + apiRoute.POST("/tags/new", api.TagsNew) + apiRoute.POST("/tags/delete", api.TagsDelete) apiRoute.POST("/settings/user/display_name", api.UserUpdateDisplayName) } } -- 2.44.1