diff options
| author | 2025-10-24 00:28:40 +0700 | |
|---|---|---|
| committer | 2025-10-24 00:28:40 +0700 | |
| commit | adcd8cf82f7ec4027701643c545b1f27a7cfc221 (patch) | |
| tree | d3a0d95c851108447b6ce55b5326943d3581126c | |
| parent | f101d6a555c6d3453def332f2c230b37ae5c81b4 (diff) | |
| download | web-adcd8cf82f7ec4027701643c545b1f27a7cfc221.tar.gz web-adcd8cf82f7ec4027701643c545b1f27a7cfc221.zip | |
feat: send new blog posts to subscribers
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 9 | ||||
| -rw-r--r-- | internal/b2/client.go | 7 | ||||
| -rw-r--r-- | internal/blogtrigger/blogtrigger.go | 68 | ||||
| -rw-r--r-- | internal/mailer/mailer.go | 103 | ||||
| -rw-r--r-- | internal/router/api-v1-email-send-verification-code.go | 2 | ||||
| -rw-r--r-- | internal/router/api-v1-general-page-body.go | 4 | ||||
| -rw-r--r-- | locale/localization.en.yaml | 5 | ||||
| -rw-r--r-- | locale/localization.go | 9 | ||||
| -rw-r--r-- | locale/localization.ru.yaml | 3 | ||||
| -rw-r--r-- | main.go | 14 | ||||
| -rw-r--r-- | views/layouts/general-mail.html | 41 | ||||
| -rw-r--r-- | views/layouts/general-page.html | 6 | ||||
| -rw-r--r-- | views/messages/new-post.html | 20 | ||||
| -rw-r--r-- | views/pages/global-map.html | 2 | ||||
| -rw-r--r-- | views/partials/catalogue-blog-cards.html | 2 |
16 files changed, 271 insertions, 29 deletions
@@ -19,18 +19,21 @@ require ( github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/go-co-op/gocron/v2 v2.17.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/stretchr/testify v1.10.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.65.0 // indirect github.com/wneessen/go-mail v0.7.2 // indirect @@ -13,6 +13,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4= +github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -32,6 +34,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -51,10 +55,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8= @@ -78,5 +86,6 @@ golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/b2/client.go b/internal/b2/client.go index 7b745a8..d712736 100644 --- a/internal/b2/client.go +++ b/internal/b2/client.go @@ -3,6 +3,7 @@ package b2 import ( "context" "fmt" + "slices" "strings" "time" @@ -34,6 +35,7 @@ func NewB2Client(cfg *config.B2Config) (*B2Client, error) { type BlogPage struct { Link string FileName string + Lang string Metadata *frontmatter.Metadata } @@ -74,6 +76,9 @@ func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) { nameParts := strings.Split(linkParts[len(linkParts)-1], ".") fileName := strings.Join(nameParts[:len(linkParts)-1], ".") + tags := strings.Split(attrs.Info["tags"], ",") + slices.Sort(tags) + filePaths = append(filePaths, &BlogPage{ Link: obj.Name(), FileName: fileName, @@ -83,7 +88,7 @@ func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) { ActionDate: attrs.Info["action-date"], PublishedTime: publishedTime, Thumbnail: attrs.Info["thumbnail"], - Tags: strings.Split(attrs.Info["tags"], ","), + Tags: tags, Geolocation: attrs.Info["geolocation"], }, }) diff --git a/internal/blogtrigger/blogtrigger.go b/internal/blogtrigger/blogtrigger.go new file mode 100644 index 0000000..ab66981 --- /dev/null +++ b/internal/blogtrigger/blogtrigger.go @@ -0,0 +1,68 @@ +package blogtrigger + +import ( + "fmt" + "log/slog" + + "github.com/SayaAndy/saya-today-web/config" + "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/go-co-op/gocron/v2" +) + +type BlogTriggerScheduler struct { + s gocron.Scheduler + knownBlogPages map[string]map[string]*b2.BlogPage + b2Client *b2.B2Client + onTrigger func([]*b2.BlogPage) error +} + +func NewBlogTriggerScheduler(b2Client *b2.B2Client, availableLanguages []config.AvailableLanguageConfig, onTrigger func([]*b2.BlogPage) error) (*BlogTriggerScheduler, error) { + s, err := gocron.NewScheduler() + if err != nil { + return nil, fmt.Errorf("failed to create new scheduler: %w", err) + } + + knownBlogPages := make(map[string]map[string]*b2.BlogPage, len(availableLanguages)) + for _, lang := range availableLanguages { + knownBlogPages[lang.Name] = make(map[string]*b2.BlogPage) + } + + bts := &BlogTriggerScheduler{s, knownBlogPages, b2Client, onTrigger} + defer bts.s.Start() + + bts.s.NewJob(gocron.CronJob("0/5 * * * *", false), gocron.NewTask(func(bts *BlogTriggerScheduler) { + posts, err := bts.scan() + if err != nil { + slog.Error("failed to execute scanning new blog pages cron job", slog.String("error", err.Error())) + return + } + if err = onTrigger(posts); err != nil { + slog.Error("error happened on callback function after scanning new blog pages", slog.String("error", err.Error())) + return + } + }, bts)) + + if _, err = bts.scan(); err != nil { + return nil, fmt.Errorf("failed to scan existing blog pages in b2: %w", err) + } + + return bts, nil +} + +func (bts *BlogTriggerScheduler) scan() (newPages []*b2.BlogPage, err error) { + newPages = make([]*b2.BlogPage, 0) + for lang := range bts.knownBlogPages { + posts, err := bts.b2Client.Scan(lang + "/") + if err != nil { + return nil, fmt.Errorf("failed to scan blog pages in b2 on '%s': %w", lang, err) + } + for _, post := range posts { + if _, ok := bts.knownBlogPages[lang][post.FileName]; !ok { + post.Lang = lang + newPages = append(newPages, post) + bts.knownBlogPages[lang][post.FileName] = post + } + } + } + return newPages, nil +} diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go index ba2202e..f51186e 100644 --- a/internal/mailer/mailer.go +++ b/internal/mailer/mailer.go @@ -7,11 +7,13 @@ import ( "encoding/binary" "fmt" "log/slog" + "slices" "strconv" "strings" "sync" "time" + "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/internal/templatemanager" "github.com/SayaAndy/saya-today-web/locale" "github.com/dgraph-io/ristretto/v2" @@ -149,16 +151,14 @@ func (m *Mailer) MailIsTaken(email string) (bool, error) { return isTaken, nil } -func (m *Mailer) GetInfo(userId string) (email string, lang string, err error) { - hash := m.GetHash(userId) - +func (m *Mailer) GetInfo(userIdHash []byte) (email string, lang string, err error) { tx, err := m.db.Begin() if err != nil { return "", "", fmt.Errorf("failed to initialize transaction with db: %s", err) } var rows *sql.Rows - if rows, err = tx.Query(`SELECT email, lang FROM user_email_table WHERE user_id=? LIMIT 1;`, hash); err != nil { + if rows, err = tx.Query(`SELECT email, lang FROM user_email_table WHERE user_id=? LIMIT 1;`, userIdHash); err != nil { tx.Rollback() return "", "", fmt.Errorf("failed to query user-email settings in db: %s", err) } @@ -330,6 +330,7 @@ func (m *Mailer) Subscribe(userId string, subscriptionType SubscriptionType, tag return fmt.Errorf("failed to initialize transaction with db: %s", err) } + slices.Sort(tags) tagsOutput := "" switch subscriptionType { case All: @@ -357,6 +358,98 @@ func (m *Mailer) Subscribe(userId string, subscriptionType SubscriptionType, tag return nil } -func SendNewPost() { +func (m *Mailer) NewPost(post *b2.BlogPage) error { + tx, err := m.db.Begin() + if err != nil { + return fmt.Errorf("failed to initialize transaction with db: %s", err) + } + + var rows *sql.Rows + if rows, err = tx.Query(`SELECT user_id, tags FROM subscription_user_to_tags_table;`); err != nil { + tx.Rollback() + return fmt.Errorf("failed to query user-to-tags table in db: %s", err) + } + + var userId []byte + usersToSend := make([]string, 0) + var tagsString string + i := -1 + +rowLoop: + for rows.Next() { + i++ + if err = rows.Scan(&userId, &tagsString); err != nil { + slog.Warn("failed to scan a row in user-to-tags table", slog.String("error", err.Error()), slog.Int("index", i), slog.String("user_id", base64.RawStdEncoding.EncodeToString(userId))) + continue + } + + if tagsString == "" { + continue + } + + email, lang, err := m.GetInfo(userId) + if err != nil { + slog.Warn("failed to get info about the user", slog.String("error", err.Error()), slog.Int("index", i), slog.String("user_id", base64.RawStdEncoding.EncodeToString(userId))) + continue + } + if lang != post.Lang { + continue + } + + if tagsString == "_all" { + usersToSend = append(usersToSend, email) + continue + } + + pageTags := strings.Split(tagsString, ",") + for _, tag := range pageTags { + if _, found := slices.BinarySearch(post.Metadata.Tags, tag); found { + usersToSend = append(usersToSend, email) + continue rowLoop + } + } + } + + tx.Commit() + rows.Close() + + msgBody, err := m.tm.Render("new-post", fiber.Map{ + "L": m.l[post.Lang], + "Lang": post.Lang, + "Post": post, + "ClientHost": m.clientHost, + }) + + messages := make([]*mail.Msg, 0, len(usersToSend)) + for _, user := range usersToSend { + if err != nil { + return fmt.Errorf("failed to render message body: %w", err) + } + + message := mail.NewMsg() + + if err := message.EnvelopeFrom(m.mailAddress); err != nil { + return fmt.Errorf("failed to set ENVELOPE FROM address: %w", err) + } + if err := message.FromFormat(m.publicName, m.mailAddress); err != nil { + return fmt.Errorf("failed to set formatted FROM address: %w", err) + } + if err := message.To(user); err != nil { + return fmt.Errorf("failed to set TO address: %w", err) + } + + message.SetMessageID() + message.SetDate() + message.SetBulk() + message.Subject(m.l[post.Lang].Mail.NewPost.Subject) + message.SetBodyString(mail.TypeTextHTML, string(msgBody)) + + messages = append(messages, message) + } + + if err := m.mailClient.DialAndSend(messages...); err != nil { + return fmt.Errorf("failed to send new post notifications: %w", err) + } + return nil } diff --git a/internal/router/api-v1-email-send-verification-code.go b/internal/router/api-v1-email-send-verification-code.go index 2cde1bd..9372eb3 100644 --- a/internal/router/api-v1-email-send-verification-code.go +++ b/internal/router/api-v1-email-send-verification-code.go @@ -58,7 +58,7 @@ func Api_V1_Email_SendVerificationCode(l map[string]*locale.LocaleConfig) func(c }) } - if previousEmail, _, _ := Mailer.GetInfo(id); previousEmail == email { + if previousEmail, _, _ := Mailer.GetInfo(Mailer.GetHash(id)); previousEmail == email { return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.EmailAlreadyValidated, map[string]string{}) } diff --git a/internal/router/api-v1-general-page-body.go b/internal/router/api-v1-general-page-body.go index 66f1234..b5567b7 100644 --- a/internal/router/api-v1-general-page-body.go +++ b/internal/router/api-v1-general-page-body.go @@ -14,6 +14,7 @@ import ( "github.com/SayaAndy/saya-today-web/config" "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/SayaAndy/saya-today-web/internal/blogtrigger" "github.com/SayaAndy/saya-today-web/internal/factgiver" "github.com/SayaAndy/saya-today-web/internal/frontmatter" "github.com/SayaAndy/saya-today-web/internal/mailer" @@ -24,6 +25,7 @@ import ( var FactGiver *factgiver.FactGiver var Mailer *mailer.Mailer +var BlogTrigger *blogtrigger.BlogTriggerScheduler func init() { assert(0, tm.Add("general-page-body", "views/partials/general-page-body.html")) @@ -110,7 +112,7 @@ func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []config.A } else if len(pathParts) == 2 && pathParts[1] == "user" { values["Title"] = l[lang].UserProfile.Header - email, _, err := Mailer.GetInfo(c.IP()) + email, _, err := Mailer.GetInfo(Mailer.GetHash(c.IP())) if err != nil { slog.Error("get info from mailer about a client", slog.String("error", err.Error())) } diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml index ce334d9..8981c49 100644 --- a/locale/localization.en.yaml +++ b/locale/localization.en.yaml @@ -26,6 +26,7 @@ Metadata: Published: 'Published' Action: 'Took place on' Mail: + UnsubscribeFooter: 'If this letter got you in a bad mood, you can unsubscribe from my blog by this link.' VerifyEmail: Subject: 'Verification Link for SAYA.TODAY is waiting for you!' Welcome: 'Greetings!' @@ -34,7 +35,9 @@ Mail: InputCode: "Or input this code on the personal page:" IfRandom: 'If you are unsure what this letter this, you can ignore it, and also, if you are interested, you may visit my site :)' NewPost: - Subject: 'New post arrived at SAYA.TODAY!' + Subject: 'A new post arrived at SAYA.TODAY!' + Intro: 'A new post came!' + CapturedOn: 'Captured on' UserProfile: Header: 'Personal Settings' EmailHeader: 'E-Mail' diff --git a/locale/localization.go b/locale/localization.go index 1028c9e..b3a1613 100644 --- a/locale/localization.go +++ b/locale/localization.go @@ -52,8 +52,9 @@ type MetadataConfig struct { } type MailConfig struct { - VerifyEmail VerifyEmailConfig `yaml:"VerifyEmail" json:"VerifyEmail"` - NewPost NewPostConfig `yaml:"NewPost" json:"NewPost"` + UnsubscribeFooter string `yaml:"UnsubscribeFooter" json:"UnsubscribeFooter"` + VerifyEmail VerifyEmailConfig `yaml:"VerifyEmail" json:"VerifyEmail"` + NewPost NewPostConfig `yaml:"NewPost" json:"NewPost"` } type VerifyEmailConfig struct { @@ -66,7 +67,9 @@ type VerifyEmailConfig struct { } type NewPostConfig struct { - Subject string `yaml:"Subject" json:"Subject"` + Subject string `yaml:"Subject" json:"Subject"` + CapturedOn string `yaml:"CapturedOn" json:"CapturedOn"` + Intro string `yaml:"Intro" json:"Intro"` } type UserProfileConfig struct { diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml index 8894e22..c09ad16 100644 --- a/locale/localization.ru.yaml +++ b/locale/localization.ru.yaml @@ -26,6 +26,7 @@ Metadata: Published: 'Опубликовано' Action: 'Время действия' Mail: + UnsubscribeFooter: 'Если данное письмо пришло вам случайно, либо вы хотите отписаться, можете перейти по этой ссылке.' VerifyEmail: Subject: 'Ссылка для верификации для подтверждения вашей почты SAYA.TODAY' Welcome: 'Приветствую!' @@ -35,6 +36,8 @@ Mail: IfRandom: 'Если ты не понимаешь, что это за спам, можешь проигнорировать это письмо, а ещё, если интересно, можешь посетить мой сайт :)' NewPost: Subject: 'Новый пост на SAYA.TODAY!' + Intro: 'Вышел новый пост!' + CapturedOn: 'Снималось' UserProfile: Header: 'Личные настройки' EmailHeader: 'E-Mail' @@ -11,6 +11,7 @@ import ( "github.com/SayaAndy/saya-today-web/config" "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/SayaAndy/saya-today-web/internal/blogtrigger" "github.com/SayaAndy/saya-today-web/internal/factgiver" "github.com/SayaAndy/saya-today-web/internal/glightbox" "github.com/SayaAndy/saya-today-web/internal/mailer" @@ -152,6 +153,19 @@ func main() { os.Exit(1) } + router.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(b2Client, cfg.AvailableLanguages, func(bp []*b2.BlogPage) error { + for _, post := range bp { + if err := router.Mailer.NewPost(post); err != nil { + return err + } + } + return nil + }) + if err != nil { + slog.Error("fail to initialize blog trigger", slog.String("error", err.Error())) + os.Exit(1) + } + app.Get("/", router.Api_V1_GeneralPage(localization, availableLanguages)) app.Get("/:lang<len(2)>", router.Api_V1_GeneralPage(localization, availableLanguages)) app.Get("/:lang/map", router.Lang_Map(localization, availableLanguages, b2Client)) diff --git a/views/layouts/general-mail.html b/views/layouts/general-mail.html index c73a1ec..8f42c7a 100644 --- a/views/layouts/general-mail.html +++ b/views/layouts/general-mail.html @@ -22,7 +22,6 @@ html { font-family: 'Philosopher', sans-serif; - color: #343b58; --noise-image: url(data:image/png;base64,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); } @@ -31,12 +30,8 @@ body { } a { - color: #c3caff !important; text-decoration: underline; -} - -a:hover { - color: #d1d6ff !important; + text-shadow: none !important; } .header { @@ -53,8 +48,8 @@ a:hover { } .content { - margin: 0rem 32px 32px 32px; - padding: 16px; + margin: 0 32px 0 32px; + padding: 16px 16px 32px 16px; font-size: 24px; text-shadow: -1px 0 #343b58, 0 1px #343b58, 1px 0 #343b58, 0 -1px #343b58; color: white; @@ -62,11 +57,35 @@ a:hover { background-image: var(--noise-image); } +.footer { + margin: 0; + padding: 1px 8px 1px 8px; + font-size: 16px; + color: white; +} + +.darkened { + background-color: #9aa5f4; + background-image: var(--noise-image); +} + .outlier { padding-inline: 4px; background-color: #6c6e75; background-image: var(--noise-image); } + +table, th, td { + border: 1px solid black; +} + +td { + padding: 5px; +} + +img { + width: 200px; +} </style> </head> @@ -75,8 +94,12 @@ a:hover { saya.today </div> - <div id="content" class="content"> + <div id="content" class="content" style="background-color: #e3e6ff !important;"> {{ block "body" . }}{{ end }} </div> + + <div id="footer" class="footer darkened"> + {{ block "footer" . }}{{ end }} + </div> </body> </html> diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html index 48a0f16..a7281e8 100644 --- a/views/layouts/general-page.html +++ b/views/layouts/general-page.html @@ -9,7 +9,7 @@ <link href="/output.css" rel="stylesheet"> </head> -<body data-theme="ram" class="font-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-[100dvh] flex flex-row"> +<body data-theme="ram" class="font-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh flex flex-row"> <script> let map = null; @@ -172,7 +172,7 @@ <div class="grow bg-split-left"></div> <div class="flex flex-col [@media(max-height:32rem)]:flex-row"> - <div id="sidebar" class="bg-sidebar flex sticky flex-col z-20 w-[100dvw] xs:w-[90dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] [@media(max-height:32rem)]:w-[5rem] h-[5rem] [@media(max-height:32rem)]:h-[100dvh] shadow-[0_0_1rem_black]"> + <div id="sidebar" class="bg-sidebar flex sticky flex-col z-20 w-dvw xs:w-[90dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] [@media(max-height:32rem)]:w-[5rem] h-[5rem] [@media(max-height:32rem)]:h-dvh shadow-[0_0_1rem_black]"> <div class="text-[3rem] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1dvw] flex flex-row [@media(max-height:32rem)]:flex-col gap-0 relative z-20"> <a href="./" id="sidebar-back-button" class="w-[5rem] h-[5rem] hidden"> <i onclick="return changeUrl('/../', true);" class="fas fa-circle-left flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i> @@ -234,7 +234,7 @@ </script> </div> - <div class="bg-background-dark @container/main flex z-10 w-[100dvw] xs:w-[90dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] grow-0 h-[calc(100dvh-5rem)] [@media(max-height:32rem)]:h-[100dvh] shadow-[0_0_1rem_black]"> + <div class="bg-background-dark @container/main flex z-10 w-dvw xs:w-[90dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] grow-0 h-[calc(100dvh-5rem)] [@media(max-height:32rem)]:h-dvh shadow-[0_0_1rem_black]"> <div class="bg-background-dark border-r-2 border-dashed border-main-hard"></div> <div class="max-h-100% flex flex-col grow bg-background-dark relative pt-4 ml-8 pr-4"> diff --git a/views/messages/new-post.html b/views/messages/new-post.html index 4fbfc02..7b3082a 100644 --- a/views/messages/new-post.html +++ b/views/messages/new-post.html @@ -1,3 +1,19 @@ {{ define "body" }} -<p>Новый пост ёпта.</p> -{{ end }}
\ No newline at end of file +<p>{{ .L.Mail.NewPost.Intro }}</p> +<table> + <tr> + <td rowspan="3"><a href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}"><img src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Post.Metadata.Thumbnail }}.webp"></a></td> + <td class="darkened" style="font-size: 24px"><a href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}">{{ .Post.Metadata.Title }}</a></td> + </tr> + <tr> + <td class="darkened" style="font-size: 16px">{{ .Post.Metadata.ShortDescription }}</td> + </tr> + <tr> + <td class="darkened" style="font-size: 16px">{{ .L.Mail.NewPost.CapturedOn }} {{ .Post.Metadata.ActionDate }}</td> + </tr> +</table> +{{ end }} + +{{ define "footer" }} +<p>{{ .L.Mail.UnsubscribeFooter }}</p> +{{ end }} diff --git a/views/pages/global-map.html b/views/pages/global-map.html index 8be99bf..306f442 100644 --- a/views/pages/global-map.html +++ b/views/pages/global-map.html @@ -12,7 +12,7 @@ <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css"> </head> -<body data-theme="ram" class="font-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-[100dvh]"> +<body data-theme="ram" class="font-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh"> <script> function switchTheme(theme) { diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html index 9bc5a30..b50f419 100644 --- a/views/partials/catalogue-blog-cards.html +++ b/views/partials/catalogue-blog-cards.html @@ -5,7 +5,7 @@ <img onclick="return changeUrl('{{ .ArticleLink }}');" class="w-full h-full aspect-square cursor-pointer object-cover rounded-lg select-none" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Thumbnail }}.webp"> </a> <div class="grow my-0.5 mx-1 flex flex-col justify-center"> - <div class="flex flex-row gap-2 max-w-[100%]"> + <div class="flex flex-row gap-2 max-w-full"> <a href="{{ .ArticleLink }}" onclick="return changeUrl('{{ .ArticleLink }}');" class="grow text-base cursor-pointer"> <span class="font-extrabold">{{ .Title }}</span> <span class="font-extrabold select-none text-secondary">//</span> |