From f9a621d56c54110d5938ad0071bbace325773070 Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Wed, 10 Sep 2025 13:55:30 +0700 Subject: feat: add page views feat: more responsive header size to keep it in one row refactor: move like and view count into separate row in blog card --- internal/router/api-v1-blog-search.go | 1 + internal/router/api-v1-general-page-body.go | 2 + internal/router/client-cache.go | 164 +++++++++++++++++++++------- views/partials/catalogue-blog-cards.html | 11 +- views/partials/general-page-header.html | 6 +- 5 files changed, 135 insertions(+), 49 deletions(-) diff --git a/internal/router/api-v1-blog-search.go b/internal/router/api-v1-blog-search.go index 946b830..69be651 100644 --- a/internal/router/api-v1-blog-search.go +++ b/internal/router/api-v1-blog-search.go @@ -79,6 +79,7 @@ func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []string, b2Clie "Thumbnail": page.Metadata.Thumbnail, "Tags": page.Metadata.Tags, "LikeCount": CCache.GetLikeCount(page.FileName), + "ViewCount": CCache.GetViewCount(page.FileName), }) break } diff --git a/internal/router/api-v1-general-page-body.go b/internal/router/api-v1-general-page-body.go index f27e0c0..b2fa268 100644 --- a/internal/router/api-v1-general-page-body.go +++ b/internal/router/api-v1-general-page-body.go @@ -138,6 +138,8 @@ func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []string, values["ParsedMarkdown"] = template.HTML(parsedMarkdown) additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") + + go CCache.View(c.IP(), pathParts[2]) } else if len(pathParts) == 1 { values["Title"] = l[lang].HomePage.Header values["FilledHeartCount"] = uint(40) diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go index e448fb3..d607cdd 100644 --- a/internal/router/client-cache.go +++ b/internal/router/client-cache.go @@ -16,6 +16,7 @@ type ClientCache struct { hashMapMutex sync.RWMutex likePageMap map[string]map[string]struct{} + viewPageMap map[string]map[string]struct{} pageMutexMap map[string]*sync.RWMutex pageMutexMapMutex sync.Mutex @@ -38,6 +39,7 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { } likePageMap := make(map[string]map[string]struct{}) + viewPageMap := make(map[string]map[string]struct{}) pageMutexMap := make(map[string]*sync.RWMutex) for rows.Next() { @@ -50,9 +52,32 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { userIdString := base64.RawStdEncoding.EncodeToString(userId) if _, ok := likePageMap[pageRef]; !ok { likePageMap[pageRef] = make(map[string]struct{}) + viewPageMap[pageRef] = make(map[string]struct{}) pageMutexMap[pageRef] = &sync.RWMutex{} } likePageMap[pageRef][userIdString] = struct{}{} + viewPageMap[pageRef][userIdString] = struct{}{} + } + + rows, err = tx.Query("select * from blog_views;") + if err != nil { + tx.Rollback() + return nil, fmt.Errorf("fail to query db for blog_views to fill cache: %w", err) + } + + for rows.Next() { + var pageRef string + var userId []byte + if err = rows.Scan(&pageRef, &userId); err != nil { + tx.Rollback() + return nil, fmt.Errorf("fail scanning blog_views to fill cache: %w", err) + } + userIdString := base64.RawStdEncoding.EncodeToString(userId) + if _, ok := viewPageMap[pageRef]; !ok { + viewPageMap[pageRef] = make(map[string]struct{}) + pageMutexMap[pageRef] = &sync.RWMutex{} + } + viewPageMap[pageRef][userIdString] = struct{}{} } if err = tx.Commit(); err != nil { @@ -62,6 +87,7 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { return &ClientCache{ hashMap: make(map[string]string), likePageMap: likePageMap, + viewPageMap: viewPageMap, pageMutexMap: pageMutexMap, salt: salt, db: db, @@ -74,51 +100,14 @@ 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 { + if err = batchSave(tx, "blog_likes", c.likePageMap); 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([]any, 0, 200) - - for pageRef, userSet := range c.likePageMap { - for userId := range userSet { - if _, ok := userIdBytes[userId]; !ok { - userIdBytes[userId], err = base64.RawStdEncoding.DecodeString(userId) - if err != nil { - slog.Warn("couldn't parse one of user hashes into bytes back", slog.String("hash", userId), slog.String("error", err.Error())) - continue - } - } - - sqlStatementVars = append(sqlStatementVars, any(pageRef), any(userIdBytes[userId])) - if len(sqlStatementVars) < 200 { - continue - } - - if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { - slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error())) - } - - sqlStatementVars = make([]any, 0, 200) - } + return fmt.Errorf("fail to save blog_likes: %s", err) } - if len(sqlStatementVars) > 0 { - sqlStatement = fmt.Sprintf(` - INSERT OR IGNORE INTO blog_likes (page_ref, user_id) - VALUES %s(?, ?); - `, strings.Repeat("(?, ?), ", len(sqlStatementVars)/2-1)) - - if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { - slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error())) - } + if err = batchSave(tx, "blog_views", c.viewPageMap); err != nil { + tx.Rollback() + return fmt.Errorf("fail to save blog_views: %s", err) } return tx.Commit() @@ -222,3 +211,94 @@ func (c *ClientCache) LikeOff(id string, page string) (alreadyUnliked bool) { delete(c.likePageMap[page], hash) return false } + +func (c *ClientCache) GetViewStatus(id string, page string) bool { + page = strings.Clone(page) + + mutex := c.getPageMutex(page) + mutex.RLock() + defer mutex.RUnlock() + + if _, ok := c.viewPageMap[page]; !ok { + return false + } + _, ok := c.viewPageMap[page][c.GetHash(id)] + return ok +} + +func (c *ClientCache) GetViewCount(page string) int { + page = strings.Clone(page) + + mutex := c.getPageMutex(page) + mutex.RLock() + defer mutex.RUnlock() + + if userSet, ok := c.viewPageMap[page]; ok { + return len(userSet) + } + return 0 +} + +func (c *ClientCache) View(id string, page string) { + page = strings.Clone(page) + hash := c.GetHash(id) + + mutex := c.getPageMutex(page) + mutex.Lock() + defer mutex.Unlock() + + if _, ok := c.viewPageMap[page]; !ok { + c.viewPageMap[page] = make(map[string]struct{}) + } + c.viewPageMap[page][hash] = struct{}{} +} + +func batchSave(tx *sql.Tx, table string, pageMap map[string]map[string]struct{}) (err error) { + if _, err = tx.Exec(fmt.Sprintf("delete from %s;", table)); err != nil { + return fmt.Errorf("fail to truncate table %s: %w", table, err) + } + + userIdBytes := make(map[string][]byte) + + sqlStatement := fmt.Sprintf(` + INSERT OR IGNORE INTO %s (page_ref, user_id) + VALUES %s(?, ?); + `, table, strings.Repeat("(?, ?), ", 99)) + sqlStatementVars := make([]any, 0, 200) + + for pageRef, userSet := range pageMap { + for userId := range userSet { + if _, ok := userIdBytes[userId]; !ok { + userIdBytes[userId], err = base64.RawStdEncoding.DecodeString(userId) + if err != nil { + slog.Warn("couldn't parse one of user hashes into bytes back", slog.String("hash", userId), slog.String("error", err.Error())) + continue + } + } + + sqlStatementVars = append(sqlStatementVars, any(pageRef), any(userIdBytes[userId])) + if len(sqlStatementVars) < 200 { + continue + } + + if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { + slog.Warn("couldn't insert blog stat pairs into db", slog.String("table", table), slog.String("error", err.Error())) + } + + sqlStatementVars = make([]any, 0, 200) + } + } + + if len(sqlStatementVars) > 0 { + sqlStatement = fmt.Sprintf(` + INSERT OR IGNORE INTO %s (page_ref, user_id) + VALUES %s(?, ?); + `, table, strings.Repeat("(?, ?), ", len(sqlStatementVars)/2-1)) + + if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { + slog.Warn("couldn't insert blog stat pairs into db", slog.String("table", table), slog.String("error", err.Error())) + } + } + + return nil +} diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html index 06d99d5..37b3f57 100644 --- a/views/partials/catalogue-blog-cards.html +++ b/views/partials/catalogue-blog-cards.html @@ -8,14 +8,17 @@ {{ .Title }} // {{ .ActionDate }} - // - - {{ .LikeCount }} -

{{ .PublishedTime }}

{{ .ShortDescription }}

+

+ + {{ .LikeCount }} + // + + {{ .ViewCount }} +

{{ $.L.TagsLabel }} {{ template "catalogue-blog-card-tags.html" . }}

diff --git a/views/partials/general-page-header.html b/views/partials/general-page-header.html index 63803bd..6cb87b7 100644 --- a/views/partials/general-page-header.html +++ b/views/partials/general-page-header.html @@ -1,8 +1,8 @@
-

//

-

{{ .Title }}

-

{{ .PublishedDate }}

+

//

+

{{ .Title }}

+

{{ .PublishedDate }}

{{ block "header" . }}{{ end }} -- cgit v1.3.1+13 From e372d8ad15ca9026fffb9248991e3df48c72b5f6 Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Wed, 10 Sep 2025 14:12:45 +0700 Subject: feat: add contacts in main page --- locale/localization.en.yaml | 1 + locale/localization.go | 1 + locale/localization.ru.yaml | 1 + views/pages/home-page.html | 22 ++++++++++++++++++++++ 4 files changed, 25 insertions(+) diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml index b3fca58..1b3654b 100644 --- a/locale/localization.en.yaml +++ b/locale/localization.en.yaml @@ -11,6 +11,7 @@ GlobalMap: Header: 'Global Map' HomePage: Header: 'Home Page' + Contacts: 'Contacts' DidYouKnowThat: 'Did you know, that...' SidebarDescription: 'Describing the Sidebar' HomePageDescription: 'Home Page -- where we are right now!' diff --git a/locale/localization.go b/locale/localization.go index 5917932..088783f 100644 --- a/locale/localization.go +++ b/locale/localization.go @@ -30,6 +30,7 @@ type GlobalMapConfig struct { type HomePageConfig struct { Header string `yaml:"Header" json:"Header"` + Contacts string `yaml:"Contacts" json:"Contacts"` DidYouKnowThat string `yaml:"DidYouKnowThat" json:"DidYouKnowThat"` SidebarDescription string `yaml:"SidebarDescription" json:"SidebarDescription"` HomePageDescription string `yaml:"HomePageDescription" json:"HomePageDescription"` diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml index 5e8d39f..9d7084f 100644 --- a/locale/localization.ru.yaml +++ b/locale/localization.ru.yaml @@ -11,6 +11,7 @@ GlobalMap: Header: 'Глобальная карта' HomePage: Header: 'Домашняя страница' + Contacts: 'Контакты' DidYouKnowThat: 'А вы знали, что...' SidebarDescription: 'Поясняем за сайдбар' HomePageDescription: 'Домашняя страница -- где мы сейчас и находимся!' diff --git a/views/pages/home-page.html b/views/pages/home-page.html index 9775ac3..6fd716b 100644 --- a/views/pages/home-page.html +++ b/views/pages/home-page.html @@ -10,6 +10,7 @@

{{ .L.HomePage.Hymn3 }}

{{ .L.HomePage.Hymn4 }}

+

+

+
{{- range $_ := iterate .FilledHeartCount }}
-- cgit v1.3.1+13