summaryrefslogtreecommitdiff
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
-rw-r--r--internal/router/api-v1-blog-search.go1
-rw-r--r--internal/router/client-cache.go99
-rw-r--r--internal/router/lang-blog.go2
-rw-r--r--main.go11
-rw-r--r--static/favicon.icobin0 -> 5694 bytes
-rw-r--r--static/input.css4
-rw-r--r--views/pages/blog-catalogue.html75
-rw-r--r--views/partials/catalogue-blog-cards.html3
8 files changed, 121 insertions, 74 deletions
diff --git a/internal/router/api-v1-blog-search.go b/internal/router/api-v1-blog-search.go
index a809783..e22fabe 100644
--- a/internal/router/api-v1-blog-search.go
+++ b/internal/router/api-v1-blog-search.go
@@ -69,6 +69,7 @@ func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []string, b2Clie
"ShortDescription": page.Metadata.ShortDescription,
"Thumbnail": page.Metadata.Thumbnail,
"Tags": page.Metadata.Tags,
+ "LikeCount": CCache.GetLikeCount(page.FileName),
})
break
}
diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go
index 56cd595..81d7641 100644
--- a/internal/router/client-cache.go
+++ b/internal/router/client-cache.go
@@ -18,11 +18,14 @@ type PageLike struct {
type ClientCache struct {
hashMap map[string]string
- mutexLikeMap map[string]*sync.Mutex
- mutexHashMap map[string]*sync.Mutex
- likePageMap map[string]map[string]struct{}
- salt []byte
- db *sql.DB
+ hashMapMutex sync.RWMutex
+
+ likePageMap map[string]map[string]struct{}
+ pageMutexMap map[string]*sync.RWMutex
+ pageMutexMapMutex sync.Mutex
+
+ salt []byte
+ db *sql.DB
}
var CCache *ClientCache
@@ -40,20 +43,21 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) {
}
likePageMap := make(map[string]map[string]struct{})
+ pageMutexMap := make(map[string]*sync.RWMutex)
for rows.Next() {
- pageRef := make([]byte, 32)
- userId := make([]byte, 32)
+ var pageRef string
+ var userId []byte
if err = rows.Scan(&pageRef, &userId); err != nil {
tx.Rollback()
return nil, fmt.Errorf("fail scanning blog_likes to fill cache: %w", err)
}
- pageRefString := string(pageRef)
userIdString := base64.RawStdEncoding.EncodeToString(userId)
- if _, ok := likePageMap[pageRefString]; !ok {
- likePageMap[pageRefString] = make(map[string]struct{})
+ if _, ok := likePageMap[pageRef]; !ok {
+ likePageMap[pageRef] = make(map[string]struct{})
+ pageMutexMap[pageRef] = &sync.RWMutex{}
}
- likePageMap[pageRefString][userIdString] = struct{}{}
+ likePageMap[pageRef][userIdString] = struct{}{}
}
if err = tx.Commit(); err != nil {
@@ -62,9 +66,8 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) {
return &ClientCache{
hashMap: make(map[string]string),
- mutexLikeMap: make(map[string]*sync.Mutex),
- mutexHashMap: make(map[string]*sync.Mutex),
likePageMap: likePageMap,
+ pageMutexMap: pageMutexMap,
salt: salt,
db: db,
}, nil
@@ -76,17 +79,20 @@ func (c *ClientCache) Close() error {
return fmt.Errorf("fail to init transaction with db to dump cache: %w", err)
}
+ if _, err = tx.Exec("delete from blog_likes;"); err != nil {
+ tx.Rollback()
+ return fmt.Errorf("fail to truncate table blog_likes: %w", err)
+ }
+
userIdBytes := make(map[string][]byte)
sqlStatement := fmt.Sprintf(`
INSERT OR IGNORE INTO blog_likes (page_ref, user_id)
VALUES %s(?, ?);
`, strings.Repeat("(?, ?), ", 99))
- sqlStatementVars := make([]interface{}, 0, 200)
+ sqlStatementVars := make([]any, 0, 200)
for pageRef, userSet := range c.likePageMap {
- pageRefBytes := []byte(pageRef)
-
for userId := range userSet {
if _, ok := userIdBytes[userId]; !ok {
userIdBytes[userId], err = base64.RawStdEncoding.DecodeString(userId)
@@ -96,7 +102,7 @@ func (c *ClientCache) Close() error {
}
}
- sqlStatementVars = append(sqlStatementVars, interface{}(pageRefBytes), interface{}(userIdBytes[userId]))
+ sqlStatementVars = append(sqlStatementVars, any(pageRef), any(userIdBytes[userId]))
if len(sqlStatementVars) < 200 {
continue
}
@@ -105,7 +111,7 @@ func (c *ClientCache) Close() error {
slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error()))
}
- sqlStatementVars = make([]interface{}, 0, 200)
+ sqlStatementVars = make([]any, 0, 200)
}
}
@@ -124,16 +130,16 @@ func (c *ClientCache) Close() error {
}
func (c *ClientCache) GetHash(id string) string {
+ c.hashMapMutex.RLock()
if val, ok := c.hashMap[id]; ok {
+ c.hashMapMutex.RUnlock()
slog.Debug("gave an old hash", slog.String("hash", val))
return val
}
+ c.hashMapMutex.RUnlock()
- if _, ok := c.mutexHashMap[id]; !ok {
- c.mutexHashMap[id] = &sync.Mutex{}
- }
- c.mutexHashMap[id].Lock()
- defer c.mutexHashMap[id].Unlock()
+ c.hashMapMutex.Lock()
+ defer c.hashMapMutex.Unlock()
if val, ok := c.hashMap[id]; ok {
slog.Debug("gave a newly generated hash", slog.String("hash", val))
@@ -145,7 +151,25 @@ func (c *ClientCache) GetHash(id string) string {
return c.hashMap[id]
}
+func (c *ClientCache) getPageMutex(page string) *sync.RWMutex {
+ c.pageMutexMapMutex.Lock()
+ defer c.pageMutexMapMutex.Unlock()
+
+ if mutex, ok := c.pageMutexMap[page]; ok {
+ return mutex
+ }
+
+ c.pageMutexMap[page] = &sync.RWMutex{}
+ return c.pageMutexMap[page]
+}
+
func (c *ClientCache) GetLikeStatus(id string, page string) bool {
+ page = strings.Clone(page)
+
+ mutex := c.getPageMutex(page)
+ mutex.RLock()
+ defer mutex.RUnlock()
+
if _, ok := c.likePageMap[page]; !ok {
return false
}
@@ -154,6 +178,12 @@ func (c *ClientCache) GetLikeStatus(id string, page string) bool {
}
func (c *ClientCache) GetLikeCount(page string) int {
+ page = strings.Clone(page)
+
+ mutex := c.getPageMutex(page)
+ mutex.RLock()
+ defer mutex.RUnlock()
+
if userSet, ok := c.likePageMap[page]; ok {
return len(userSet)
}
@@ -161,14 +191,13 @@ func (c *ClientCache) GetLikeCount(page string) int {
}
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()
-
+ page = strings.Clone(page)
hash := c.GetHash(id)
+ mutex := c.getPageMutex(page)
+ mutex.Lock()
+ defer mutex.Unlock()
+
if userSet, ok := c.likePageMap[page]; ok {
_, alreadyLiked = userSet[hash]
c.likePageMap[page][hash] = struct{}{}
@@ -181,16 +210,16 @@ func (c *ClientCache) LikeOn(id string, page string) (alreadyLiked bool) {
}
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()
+ page = strings.Clone(page)
+ hash := c.GetHash(id)
+
+ mutex := c.getPageMutex(page)
+ mutex.Lock()
+ defer mutex.Unlock()
if _, ok := c.likePageMap[page]; !ok {
return true
}
- hash := c.GetHash(id)
if _, ok := c.likePageMap[page][hash]; !ok {
return true
}
diff --git a/internal/router/lang-blog.go b/internal/router/lang-blog.go
index e5220c6..dd3f079 100644
--- a/internal/router/lang-blog.go
+++ b/internal/router/lang-blog.go
@@ -83,7 +83,7 @@ func Lang_Blog(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B
"Title": l[lang].BlogSearch.Header,
})
if err != nil {
- slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog"), slog.String("error", err.Error()))
+ slog.Warn("failed to generate page", slog.String("path", c.Path()), slog.String("error", err.Error()))
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
}
diff --git a/main.go b/main.go
index 2f08ba6..020f8ef 100644
--- a/main.go
+++ b/main.go
@@ -143,7 +143,14 @@ func main() {
<-sigChan
slog.Info("gracefully shutting down...")
- app.Shutdown()
- router.CCache.Close()
+ if err = app.Shutdown(); err != nil {
+ slog.Error("fail to shutdown fiber server", slog.String("error", err.Error()))
+ }
+ if err = router.CCache.Close(); err != nil {
+ slog.Error("fail to dump cache", slog.String("error", err.Error()))
+ }
+ if err = db.Close(); err != nil {
+ slog.Error("fail to close db connection", slog.String("error", err.Error()))
+ }
db.Close()
}
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..33eb226
--- /dev/null
+++ b/static/favicon.ico
Binary files differ
diff --git a/static/input.css b/static/input.css
index f254970..47e7d16 100644
--- a/static/input.css
+++ b/static/input.css
@@ -581,6 +581,10 @@ body[data-theme~="ram"] .leaflet-tile {
width: 100vw;
}
+ .ar-lt-0\.8\:max-h-\[25\%\] {
+ max-height: 25%;
+ }
+
.ar-lt-0\.8\:max-h-\[80\%\] {
max-height: 80%;
}
diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html
index 25b0685..1d0a2de 100644
--- a/views/pages/blog-catalogue.html
+++ b/views/pages/blog-catalogue.html
@@ -1,44 +1,47 @@
{{ define "body" }}
<div class="flex flex-row ar-lt-0.8:flex-col max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)] flex-1 text-[1vmax]">
<form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML"
- class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] flex shrink-0 flex-col overflow-y-auto ar-gt-0.8:mr-[1.2vmin]">
- <fieldset>
- <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.TagsHeader }}</legend>
- <div class="flex flex-col ar-lt-0.8:grid grid-cols-5 grid-flow-row-dense">
- <div class="m-[0.4vmin]">
- <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();"> {{ .L.BlogSearch.ChooseAllTags }}</label>
+ class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] ar-lt-0.8:max-h-[25%] flex shrink-0 flex-col ar-gt-0.8:mr-[1.2vmin]">
+ <div class="flex flex-col overflow-y-auto">
+ <fieldset>
+ <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.TagsHeader }}</legend>
+ <div class="flex flex-col ar-lt-0.8:grid grid-cols-5 grid-flow-row-dense">
+ <div class="m-[0.4vmin]">
+ <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();"> {{ .L.BlogSearch.ChooseAllTags }}</label>
+ </div>
+ {{- range .Tags }}
+ <div class="m-[0.4vmin]">
+ <label><input type="checkbox" id="tag{{ .Name }}Checkbox" name="tags[]" value="{{ .Name }}" onclick="contextAll();" {{ if contains $.QueryTags .Name }}checked{{ end }}> {{ .Name }} <span class="text-secondary italic">{{ .Count }}</span></label>
+ </div>
+ {{- end }}
</div>
- {{- range .Tags }}
- <div class="m-[0.4vmin]">
- <label><input type="checkbox" id="tag{{ .Name }}Checkbox" name="tags[]" value="{{ .Name }}" onclick="contextAll();" {{ if contains $.QueryTags .Name }}checked{{ end }}> {{ .Name }} <span class="text-secondary italic">{{ .Count }}</span></label>
+ </fieldset>
+ <fieldset class="mt-[0.8vmin]">
+ <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.OrderByHeader }}</legend>
+ <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col">
+ <div class="m-[0.4vmin]">
+ <label><input type="radio" id="sortTitleAsc" name="sort" value="titleAsc" {{ if eq .QuerySort "titleAsc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-a-z"></i></label>
+ </div>
+ <div class="m-[0.4vmin]">
+ <label><input type="radio" id="sortTitleDesc" name="sort" value="titleDesc" {{ if eq .QuerySort "titleDesc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-z-a"></i></label>
+ </div>
+ <div class="m-[0.4vmin]">
+ <label><input type="radio" id="sortActionDateAsc" name="sort" value="actionDateAsc" {{ if eq .QuerySort "actionDateAsc" }}checked{{ end }}> {{ .L.BlogSearch.ActionDateOrdered }} <i class="fas fa-arrow-down-1-9"></i></label>
+ </div>
+ <div class="m-[0.4vmin]">
+ <label><input type="radio" id="sortActionDateDesc" name="sort" value="actionDateDesc" {{ if eq .QuerySort "actionDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.ActionDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label>
+ </div>
+ <div class="m-[0.4vmin]">
+ <label><input type="radio" id="sortPublicationDateAsc" name="sort" value="publicationDateAsc" {{ if eq .QuerySort "publicationDateAsc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-1-9"></i></label>
+ </div>
+ <div class="m-[0.4vmin]">
+ <label><input type="radio" id="sortPublicationDateDesc" name="sort" value="publicationDateDesc" {{ if eq .QuerySort "publicationDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label>
+ </div>
</div>
- {{- end }}
- </div>
- </fieldset>
- <fieldset class="mt-[0.8vmin]">
- <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.OrderByHeader }}</legend>
- <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col">
- <div class="m-[0.4vmin]">
- <label><input type="radio" id="sortTitleAsc" name="sort" value="titleAsc" {{ if eq .QuerySort "titleAsc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-a-z"></i></label>
- </div>
- <div class="m-[0.4vmin]">
- <label><input type="radio" id="sortTitleDesc" name="sort" value="titleDesc" {{ if eq .QuerySort "titleDesc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-z-a"></i></label>
- </div>
- <div class="m-[0.4vmin]">
- <label><input type="radio" id="sortActionDateAsc" name="sort" value="actionDateAsc" {{ if eq .QuerySort "actionDateAsc" }}checked{{ end }}> {{ .L.BlogSearch.ActionDateOrdered }} <i class="fas fa-arrow-down-1-9"></i></label>
- </div>
- <div class="m-[0.4vmin]">
- <label><input type="radio" id="sortActionDateDesc" name="sort" value="actionDateDesc" {{ if eq .QuerySort "actionDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.ActionDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label>
- </div>
- <div class="m-[0.4vmin]">
- <label><input type="radio" id="sortPublicationDateAsc" name="sort" value="publicationDateAsc" {{ if eq .QuerySort "publicationDateAsc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-1-9"></i></label>
- </div>
- <div class="m-[0.4vmin]">
- <label><input type="radio" id="sortPublicationDateDesc" name="sort" value="publicationDateDesc" {{ if eq .QuerySort "publicationDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label>
- </div>
- </div>
- </fieldset>
- <div class="flex flex-row bg-background-light my-[2vmin] p-[0.4vmin] rounded-[0.4vmin] justify-items-center">
+ </fieldset>
+ </div>
+ <hr class="border-t-[0.2vmax] border-dotted border-main-dark my-[0.4vmin]">
+ <div class="flex flex-row grow bg-background-light my-[2vmin] p-[0.4vmin] rounded-[0.4vmin] justify-items-center">
<i onclick="defaultSearchParams(); cleanHrefParams();" class="fas fa-rotate-left flex-1 text-center text-[1.5vmax] hover:bg-background-dark cursor-pointer transition-colors duration-300"></i>
<div class="w-[0.5vmin] bg-background-dark grow-0"></div>
<button class="flex-1" type="submit">
diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html
index 62e931a..35c7ad7 100644
--- a/views/partials/catalogue-blog-cards.html
+++ b/views/partials/catalogue-blog-cards.html
@@ -8,6 +8,9 @@
<span class="font-extrabold">{{ .Title }}</span>
<span class="font-extrabold select-none text-secondary">//</span>
<span class="italic text-secondary">{{ .ActionDate }}</span>
+ <span class="font-extrabold select-none text-secondary">//</span>
+ <i class="fas fa-thumbs-up w-[1vmax] h-[1vmax]"></i>
+ <span class="italic text-secondary">{{ .LikeCount }}</span>
</a>
<p class="flex-3/12 grow-0 text-[1vmax] italic text-secondary text-right">{{ .PublishedTime }}</p>
</div>