From b0ab2ed92870d8c651267db10d68eb695da2ec62 Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Tue, 26 Aug 2025 23:41:00 +0700 Subject: feat: like button (only inmem-cache for now) --- internal/router/api-v1-like.go | 117 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 internal/router/api-v1-like.go (limited to 'internal/router/api-v1-like.go') diff --git a/internal/router/api-v1-like.go b/internal/router/api-v1-like.go new file mode 100644 index 0000000..1732c77 --- /dev/null +++ b/internal/router/api-v1-like.go @@ -0,0 +1,117 @@ +package router + +import ( + "fmt" + "log/slog" + "net/url" + "strconv" + "strings" + + "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/gofiber/fiber/v2" +) + +func init() { + tm.Add("blog-page-like-button", "views/partials/blog-page-like-button.html") +} + +func Api_V1_Like_Put(b2 *b2.B2Client) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + referer := c.Get("Referer", "") + if referer == "" { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") + } + urlStruct, err := url.ParseRequestURI(referer) + if err != nil { + return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) + } + + path := urlStruct.EscapedPath() + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) != 3 { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/blog/{page}'") + } + + lang, page := pathParts[0], pathParts[2] + + pageLink := lang + "/" + page + ".md" + if pages, _ := b2.Scan(pageLink); len(pages) == 0 { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server did not find '%s' article", pageLink)) + } + + ip := c.IP() + newLikeStatus, err := strconv.ParseBool(c.FormValue("like", "true")) + if err != nil { + return c.Status(fiber.ErrBadRequest.Code).SendString("invalid 'like' value") + } + + if newLikeStatus { + CCache.LikeOn(ip, page) + } else { + CCache.LikeOff(ip, page) + } + + slog.Debug("someone pressed the like button!", slog.String("ip", ip), slog.String("page", page), slog.String("new_like_status", fmt.Sprint(newLikeStatus))) + if c.Get("HX-Request", "false") == "true" { + content, err := tm.Render("blog-page-like-button", fiber.Map{ + "Liked": newLikeStatus, + }) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Send(content) + } + + return c.Status(fiber.StatusOK).SendString(fmt.Sprint(newLikeStatus)) + } +} + +func Api_V1_Like_Get(b2 *b2.B2Client) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + referer := c.Get("Referer", "") + if referer == "" { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") + } + urlStruct, err := url.ParseRequestURI(referer) + if err != nil { + return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) + } + + path := urlStruct.EscapedPath() + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) != 3 { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/blog/{page}'") + } + + lang, page := pathParts[0], pathParts[2] + + pageLink := lang + "/" + page + ".md" + if pages, _ := b2.Scan(pageLink); len(pages) == 0 { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server did not find '%s' article", pageLink)) + } + + ip := c.IP() + likeStatus := CCache.GetLikeStatus(ip, page) + + slog.Debug("someone requested the like status!", slog.String("ip", ip), slog.String("page", page), slog.Bool("like_status", likeStatus)) + if c.Get("HX-Request", "false") == "true" { + content, err := tm.Render("blog-page-like-button", fiber.Map{ + "Liked": likeStatus, + }) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Send(content) + } + + return c.Status(fiber.StatusOK).SendString(fmt.Sprint(likeStatus)) + } +} -- cgit v1.3.1+13 From 26c77e33356a519b3caaed44fd9d7f8722f41918 Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Wed, 27 Aug 2025 19:11:16 +0700 Subject: feat: add and show like count feat: localize like button --- internal/router/api-v1-like.go | 15 ++++++++++----- internal/router/client-cache.go | 7 +++++++ locale/localization.en.yaml | 1 + locale/localization.go | 1 + locale/localization.ru.yaml | 1 + main.go | 4 ++-- views/partials/blog-page-like-button.html | 5 +++-- 7 files changed, 25 insertions(+), 9 deletions(-) (limited to 'internal/router/api-v1-like.go') diff --git a/internal/router/api-v1-like.go b/internal/router/api-v1-like.go index 1732c77..327dafa 100644 --- a/internal/router/api-v1-like.go +++ b/internal/router/api-v1-like.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/SayaAndy/saya-today-web/locale" "github.com/gofiber/fiber/v2" ) @@ -15,7 +16,7 @@ func init() { tm.Add("blog-page-like-button", "views/partials/blog-page-like-button.html") } -func Api_V1_Like_Put(b2 *b2.B2Client) func(c *fiber.Ctx) error { +func Api_V1_Like_Put(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) @@ -41,12 +42,12 @@ func Api_V1_Like_Put(b2 *b2.B2Client) func(c *fiber.Ctx) error { return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server did not find '%s' article", pageLink)) } - ip := c.IP() newLikeStatus, err := strconv.ParseBool(c.FormValue("like", "true")) if err != nil { return c.Status(fiber.ErrBadRequest.Code).SendString("invalid 'like' value") } + ip := c.IP() if newLikeStatus { CCache.LikeOn(ip, page) } else { @@ -56,7 +57,9 @@ func Api_V1_Like_Put(b2 *b2.B2Client) func(c *fiber.Ctx) error { slog.Debug("someone pressed the like button!", slog.String("ip", ip), slog.String("page", page), slog.String("new_like_status", fmt.Sprint(newLikeStatus))) if c.Get("HX-Request", "false") == "true" { content, err := tm.Render("blog-page-like-button", fiber.Map{ - "Liked": newLikeStatus, + "L": l[lang], + "Liked": newLikeStatus, + "LikedCount": CCache.GetLikeCount(page), }) if err != nil { slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) @@ -70,7 +73,7 @@ func Api_V1_Like_Put(b2 *b2.B2Client) func(c *fiber.Ctx) error { } } -func Api_V1_Like_Get(b2 *b2.B2Client) func(c *fiber.Ctx) error { +func Api_V1_Like_Get(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) @@ -102,7 +105,9 @@ func Api_V1_Like_Get(b2 *b2.B2Client) func(c *fiber.Ctx) error { slog.Debug("someone requested the like status!", slog.String("ip", ip), slog.String("page", page), slog.Bool("like_status", likeStatus)) if c.Get("HX-Request", "false") == "true" { content, err := tm.Render("blog-page-like-button", fiber.Map{ - "Liked": likeStatus, + "L": l[lang], + "Liked": likeStatus, + "LikedCount": CCache.GetLikeCount(page), }) if err != nil { slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go index 3104a66..7d276b1 100644 --- a/internal/router/client-cache.go +++ b/internal/router/client-cache.go @@ -58,6 +58,13 @@ func (c *ClientCache) GetLikeStatus(id string, page string) bool { return ok } +func (c *ClientCache) GetLikeCount(page string) int { + if userSet, ok := c.likePageMap[page]; ok { + return len(userSet) + } + return 0 +} + func (c *ClientCache) LikeOn(id string, page string) (alreadyLiked bool) { if _, ok := c.mutexLikeMap[id]; !ok { c.mutexLikeMap[id] = &sync.Mutex{} diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml index f5ee6c9..48ef9da 100644 --- a/locale/localization.en.yaml +++ b/locale/localization.en.yaml @@ -9,3 +9,4 @@ BlogSearch: ChooseAllTags: 'Choose All' GlobalMap: Header: 'Global Map' +LikeButton: 'Like!' diff --git a/locale/localization.go b/locale/localization.go index 56b6462..b557e08 100644 --- a/locale/localization.go +++ b/locale/localization.go @@ -11,6 +11,7 @@ type LocaleConfig struct { TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"` BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"` GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"` + LikeButton string `yaml:"LikeButton" json:"LikeButton"` } type BlogSearchConfig struct { diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml index c236d1e..d6e9fe4 100644 --- a/locale/localization.ru.yaml +++ b/locale/localization.ru.yaml @@ -9,3 +9,4 @@ BlogSearch: ChooseAllTags: 'Выбрать все' GlobalMap: Header: 'Глобальная карта' +LikeButton: 'Нраица!' diff --git a/main.go b/main.go index 1853721..36463ff 100644 --- a/main.go +++ b/main.go @@ -88,8 +88,8 @@ func main() { app.Get("/:lang/blog/:title", router.Lang_Blog_Title(localization, availableLanguages, b2Client, md)) app.Get("/api/v1/tz", router.Api_V1_TZ()) app.Get("/api/v1/blog-search", router.Api_V1_BlogSearch(localization, availableLanguages, b2Client)) - app.Get("/api/v1/like", router.Api_V1_Like_Get(b2Client)) - app.Put("/api/v1/like", router.Api_V1_Like_Put(b2Client)) + app.Get("/api/v1/like", router.Api_V1_Like_Get(localization, b2Client)) + app.Put("/api/v1/like", router.Api_V1_Like_Put(localization, b2Client)) app.Static("/", "./static") diff --git a/views/partials/blog-page-like-button.html b/views/partials/blog-page-like-button.html index 50a9d7a..afc3027 100644 --- a/views/partials/blog-page-like-button.html +++ b/views/partials/blog-page-like-button.html @@ -1,6 +1,7 @@ \ No newline at end of file -- cgit v1.3.1+13