summaryrefslogtreecommitdiffci
diff options
from:
to:
context:
space:
mode:
-rw-r--r--internal/router/api-v1-like.go117
-rw-r--r--internal/router/client-cache.go82
-rw-r--r--internal/router/lang-blog-title.go1
-rw-r--r--main.go2
-rw-r--r--static/input.css36
-rw-r--r--views/layouts/general-page.html14
-rw-r--r--views/pages/blog-catalogue.html2
-rw-r--r--views/pages/blog-page.html6
-rw-r--r--views/pages/global-map.html2
-rw-r--r--views/partials/blog-page-like-button.html6
10 files changed, 241 insertions, 27 deletions
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))
+ }
+}
diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go
index a4a0cc2..3104a66 100644
--- a/internal/router/client-cache.go
+++ b/internal/router/client-cache.go
@@ -9,40 +9,90 @@ import (
)
type ClientCache struct {
- hashMap map[string][]byte
- mutexMap map[string]*sync.Mutex
- salt []byte
+ hashMap map[string]string
+ mutexLikeMap map[string]*sync.Mutex
+ mutexHashMap map[string]*sync.Mutex
+ likePageMap map[string]map[string]struct{}
+ salt []byte
}
var CCache *ClientCache
func NewClientCache(salt []byte) *ClientCache {
return &ClientCache{
- hashMap: make(map[string][]byte),
- mutexMap: make(map[string]*sync.Mutex),
- salt: salt,
+ hashMap: make(map[string]string),
+ mutexLikeMap: make(map[string]*sync.Mutex),
+ mutexHashMap: make(map[string]*sync.Mutex),
+ likePageMap: make(map[string]map[string]struct{}),
+ salt: salt,
}
}
-func (c *ClientCache) GetHash(id string) []byte {
-
+func (c *ClientCache) GetHash(id string) string {
if val, ok := c.hashMap[id]; ok {
- slog.Debug("gave an old hash", slog.String("hash", base64.RawStdEncoding.EncodeToString(val)))
+ slog.Debug("gave an old hash", slog.String("hash", val))
return val
}
- if _, ok := c.mutexMap[id]; !ok {
- c.mutexMap[id] = &sync.Mutex{}
+ if _, ok := c.mutexHashMap[id]; !ok {
+ c.mutexHashMap[id] = &sync.Mutex{}
}
- c.mutexMap[id].Lock()
- defer c.mutexMap[id].Unlock()
+ c.mutexHashMap[id].Lock()
+ defer c.mutexHashMap[id].Unlock()
if val, ok := c.hashMap[id]; ok {
- slog.Debug("gave a newly generated hash", slog.String("hash", base64.RawStdEncoding.EncodeToString(val)))
+ slog.Debug("gave a newly generated hash", slog.String("hash", val))
return val
}
- c.hashMap[id] = argon2.IDKey([]byte(id), c.salt, 1, 64*1024, 4, 32)
- slog.Debug("generated hash", slog.String("hash", base64.RawStdEncoding.EncodeToString(c.hashMap[id])))
+ c.hashMap[id] = base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(id), c.salt, 1, 64*1024, 4, 32))
+ slog.Debug("generated hash", slog.String("hash", c.hashMap[id]))
return c.hashMap[id]
}
+
+func (c *ClientCache) GetLikeStatus(id string, page string) bool {
+ if _, ok := c.likePageMap[page]; !ok {
+ return false
+ }
+ _, ok := c.likePageMap[page][c.GetHash(id)]
+ return ok
+}
+
+func (c *ClientCache) LikeOn(id string, page string) (alreadyLiked bool) {
+ if _, ok := c.mutexLikeMap[id]; !ok {
+ c.mutexLikeMap[id] = &sync.Mutex{}
+ }
+ c.mutexLikeMap[id].Lock()
+ defer c.mutexLikeMap[id].Unlock()
+
+ hash := c.GetHash(id)
+
+ if userSet, ok := c.likePageMap[page]; ok {
+ _, alreadyLiked = userSet[hash]
+ c.likePageMap[page][hash] = struct{}{}
+ return
+ }
+
+ c.likePageMap[page] = make(map[string]struct{})
+ c.likePageMap[page][hash] = struct{}{}
+ return
+}
+
+func (c *ClientCache) LikeOff(id string, page string) (alreadyUnliked bool) {
+ if _, ok := c.mutexLikeMap[id]; !ok {
+ c.mutexLikeMap[id] = &sync.Mutex{}
+ }
+ c.mutexLikeMap[id].Lock()
+ defer c.mutexLikeMap[id].Unlock()
+
+ if _, ok := c.likePageMap[page]; !ok {
+ return true
+ }
+ hash := c.GetHash(id)
+ if _, ok := c.likePageMap[page][hash]; !ok {
+ return true
+ }
+
+ delete(c.likePageMap[page], hash)
+ return false
+}
diff --git a/internal/router/lang-blog-title.go b/internal/router/lang-blog-title.go
index 5b80ec8..bafb0d2 100644
--- a/internal/router/lang-blog-title.go
+++ b/internal/router/lang-blog-title.go
@@ -24,7 +24,6 @@ func Lang_Blog_Title(l map[string]*locale.LocaleConfig, langs []string, b2Client
return func(c *fiber.Ctx) error {
ip := c.IP()
slog.Debug("client entering blog page", slog.String("ip", ip), slog.String("page", c.Path()))
- go CCache.GetHash(ip)
lang := c.Params("lang")
if !slices.Contains(langs, lang) {
diff --git a/main.go b/main.go
index 9f95c04..1853721 100644
--- a/main.go
+++ b/main.go
@@ -88,6 +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.Static("/", "./static")
diff --git a/static/input.css b/static/input.css
index 9f834a6..18910eb 100644
--- a/static/input.css
+++ b/static/input.css
@@ -139,10 +139,18 @@ a:hover {
color: var(--main-medium-color);
}
+.text-main-light {
+ color: var(--main-light-color);
+}
+
.text-background-dark {
color: var(--background-dark-color);
}
+.text-background-light {
+ color: var(--background-dark-color);
+}
+
.text-secondary {
color: var(--secondary-color);
}
@@ -151,6 +159,14 @@ a:hover {
background-color: var(--main-dark-color);
}
+.bg-main-medium {
+ background-color: var(--main-medium-color);
+}
+
+.bg-main-light {
+ background-color: var(--main-light-color);
+}
+
.bg-background-dark {
background-color: var(--background-dark-color);
}
@@ -167,6 +183,26 @@ a:hover {
border-color: var(--main-medium-color);
}
+.border-main-light {
+ border-color: var(--main-light-color);
+}
+
+.border-background-dark {
+ border-color: var(--background-dark-color);
+}
+
+.border-background-light {
+ border-color: var(--background-light-color);
+}
+
+.border-inset {
+ border-style: inset;
+}
+
+.border-outset {
+ border-style: outset;
+}
+
.desktop-sidebar-custom {
width: 5vw;
background-size: calc(var(--squares-and-triangles-background-size) * 1vw);
diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html
index cd19bd0..9885d66 100644
--- a/views/layouts/general-page.html
+++ b/views/layouts/general-page.html
@@ -8,7 +8,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Patua+One&display=swap" rel="stylesheet">
- {{ block "header" . }}{{ end }}
+ {{ block "top-embeds" . }}{{ end }}
<link href="/output.css" rel="stylesheet">
</head>
@@ -66,17 +66,19 @@
<p class="text-[2vmax] font-spectral italic text-left mt-auto">{{ .Title }}</p>
<p id="published-date" class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p>
</div>
+ {{ block "header" . }}{{ end }}
<hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]">
{{ block "body" . }}{{ end }}
<div class="flex flex-row mt-[0.8vmin] mb-[0.4vmin]">
- <p class="grow text-[0.8vmax]/[0.9] font-spectral italic text-right text-secondary mr-2">
+ {{ block "footer" . }}{{ end }}
+ <p class="grow content-center text-[1vmax]/[1] font-spectral italic text-right text-secondary mr-2">
All the photos on <a href="https://saya.today/">saya.today</a> are licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> by <a href="https://t.me/EarlInisMona">Saya Andy</a> © {{ .PublishedYear }}
</p>
- <img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
- <img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
- <img src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
+ <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="CC">
+ <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="BY">
+ <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="SA">
</div>
<hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]">
@@ -209,7 +211,7 @@
});
</script>
- {{ block "footer" . }}{{ end }}
+ {{ block "bottom-embeds" . }}{{ end }}
</body>
</html>
diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html
index 9fa8ea7..25b0685 100644
--- a/views/pages/blog-catalogue.html
+++ b/views/pages/blog-catalogue.html
@@ -56,7 +56,7 @@
</div>
{{ end }}
-{{ define "footer" }}
+{{ define "bottom-embeds" }}
<script>
function selectAll() {
const tagsContainers = document.querySelectorAll('form.tags-list');
diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html
index 755744d..83860d7 100644
--- a/views/pages/blog-page.html
+++ b/views/pages/blog-page.html
@@ -1,4 +1,4 @@
-{{ define "header" }}
+{{ define "top-embeds" }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lightgallery.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-zoom.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-thumbnail.min.css">
@@ -26,6 +26,10 @@
{{ end }}
{{ define "footer" }}
+<div hx-get="/api/v1/like" hx-target="this" hx-swap="outerHTML" hx-trigger="load"></div>
+{{ end }}
+
+{{ define "bottom-embeds" }}
<script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/lightgallery.min.js"></script>
<script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-zoom.min.js"></script>
<script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-thumbnail.min.js"></script>
diff --git a/views/pages/global-map.html b/views/pages/global-map.html
index 6185076..569166f 100644
--- a/views/pages/global-map.html
+++ b/views/pages/global-map.html
@@ -263,7 +263,5 @@
});
</script>
- {{ block "footer" . }}{{ end }}
-
</body>
</html>
diff --git a/views/partials/blog-page-like-button.html b/views/partials/blog-page-like-button.html
new file mode 100644
index 0000000..50a9d7a
--- /dev/null
+++ b/views/partials/blog-page-like-button.html
@@ -0,0 +1,6 @@
+<button
+ hx-put="/api/v1/like" hx-vals='{"like": {{ not .Liked }}}' hx-target="this" hx-swap="outerHTML" hx-trigger="click"
+ class="text-[0.8vmax]/[0.9] font-spectral text-left px-[0.4vmax] py-[0.2vmax] {{ if .Liked }}bg-main-light hover:bg-main-medium{{ else }}bg-main-dark hover:bg-main-medium{{ end }} text-background-dark cursor-pointer {{ if .Liked }}border-inset{{ else }}border-outset{{ end }} border-[0.3vmax] border-background-dark">
+ <i class="fas fa-thumbs-up w-[0.8vmax] h-[0.8vmax] mr-[0.4vmax]"></i>
+ <span>Нраица!</span>
+</button> \ No newline at end of file