summaryrefslogtreecommitdiff
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build-and-deploy-prod.yml2
-rw-r--r--.github/workflows/build-and-deploy-stage.yml2
-rw-r--r--config/config.go16
-rw-r--r--config/config.local.yaml13
-rw-r--r--config/config.prod.yaml12
-rw-r--r--config/config.stage.yaml12
-rw-r--r--deploy/inventory.yml2
-rw-r--r--deploy/playbook.yml6
-rw-r--r--go.mod8
-rw-r--r--go.sum13
-rw-r--r--internal/b2/client.go7
-rw-r--r--internal/blogtrigger/blogtrigger.go72
-rw-r--r--internal/glightbox/html_renderer.go4
-rw-r--r--internal/mailer/mailer.go544
-rw-r--r--internal/router/api-v1-blog-search.go2
-rw-r--r--internal/router/api-v1-email-is-in-verification.go58
-rw-r--r--internal/router/api-v1-email-send-verification-code.go98
-rw-r--r--internal/router/api-v1-email-verify.go52
-rw-r--r--internal/router/api-v1-general-page-body.go121
-rw-r--r--internal/router/api-v1-general-page-bottom-embeds.go4
-rw-r--r--internal/router/api-v1-general-page-footer.go4
-rw-r--r--internal/router/api-v1-general-page-header.go5
-rw-r--r--internal/router/api-v1-general-page-top-embeds.go4
-rw-r--r--internal/router/api-v1-general-page.go8
-rw-r--r--internal/router/api-v1-like.go2
-rw-r--r--internal/router/api-v1-subs.go67
-rw-r--r--internal/router/client-cache.go18
-rw-r--r--internal/router/lang-blog-title.go86
-rw-r--r--internal/router/lang-blog.go98
-rw-r--r--internal/router/lang-map.go2
-rw-r--r--internal/router/lang-user-unsubscribe.go71
-rw-r--r--internal/router/root.go27
-rw-r--r--internal/templatemanager/templatemanager.go1
-rw-r--r--locale/localization.en.yaml47
-rw-r--r--locale/localization.go72
-rw-r--r--locale/localization.ru.yaml47
-rw-r--r--main.go31
-rw-r--r--migrations/2_create_subscription_tables.down.sql4
-rw-r--r--migrations/2_create_subscription_tables.up.sql18
-rw-r--r--static/input.css52
-rw-r--r--views/layouts/general-mail.html105
-rw-r--r--views/layouts/general-page.html26
-rw-r--r--views/messages/new-post.html19
-rw-r--r--views/messages/verify-email.html10
-rw-r--r--views/pages/blog-catalogue.html8
-rw-r--r--views/pages/global-map.html11
-rw-r--r--views/pages/unsubscribe-page.html30
-rw-r--r--views/pages/user-page.html205
-rw-r--r--views/partials/catalogue-blog-cards.html2
-rw-r--r--views/partials/personal-page-status.html12
50 files changed, 288 insertions, 1852 deletions
diff --git a/.github/workflows/build-and-deploy-prod.yml b/.github/workflows/build-and-deploy-prod.yml
index d57e872..a180d39 100644
--- a/.github/workflows/build-and-deploy-prod.yml
+++ b/.github/workflows/build-and-deploy-prod.yml
@@ -75,4 +75,4 @@ jobs:
ANSIBLE_HOST_KEY_CHECKING: False
working-directory: ./deploy
run: |
- ansible-playbook -i inventory.yml -l prod playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_auth_salt="${{ secrets.AUTH_SALT }}" -e saya_today_web_b2_key_id="${{ secrets.B2_KEY_ID }}" -e saya_today_web_b2_application_key="${{ secrets.B2_APPLICATION_KEY }}" -e saya_today_web_environment=prod -e saya_today_web_tag=${{ github.ref_name }} -e saya_today_web_mail_salt="${{ secrets.MAIL_SALT }}" -e saya_today_web_mail_host="${{ secrets.MAIL_HOST }}" -e saya_today_web_mail_address="${{ secrets.MAIL_ADDRESS }}" -e saya_today_web_mail_username="${{ secrets.MAIL_USERNAME }}" -e saya_today_web_mail_password="${{ secrets.MAIL_PASSWORD }}"
+ ansible-playbook -i inventory.yml -l prod playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_auth_salt=${{ secrets.AUTH_SALT }} -e saya_today_web_b2_key_id=${{ secrets.B2_KEY_ID }} -e saya_today_web_b2_application_key=${{ secrets.B2_APPLICATION_KEY }} -e saya_today_web_environment=prod -e saya_today_web_tag=${{ github.ref_name }}
diff --git a/.github/workflows/build-and-deploy-stage.yml b/.github/workflows/build-and-deploy-stage.yml
index 16e6563..0aea0a4 100644
--- a/.github/workflows/build-and-deploy-stage.yml
+++ b/.github/workflows/build-and-deploy-stage.yml
@@ -86,4 +86,4 @@ jobs:
ANSIBLE_HOST_KEY_CHECKING: False
working-directory: ./deploy
run: |
- ansible-playbook -i inventory.yml -l stage playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_auth_salt="${{ secrets.AUTH_SALT }}" -e saya_today_web_b2_key_id="${{ secrets.B2_KEY_ID }}" -e saya_today_web_b2_application_key="${{ secrets.B2_APPLICATION_KEY }}" -e saya_today_web_environment=stage -e saya_today_web_tag=commit-${{ steps.ghss_vars.outputs.sha_short }} -e saya_today_web_mail_salt="${{ secrets.MAIL_SALT }}" -e saya_today_web_mail_host="${{ secrets.MAIL_HOST }}" -e saya_today_web_mail_address="${{ secrets.MAIL_ADDRESS }}" -e saya_today_web_mail_username="${{ secrets.MAIL_USERNAME }}" -e saya_today_web_mail_password="${{ secrets.MAIL_PASSWORD }}"
+ ansible-playbook -i inventory.yml -l stage playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_auth_salt=${{ secrets.AUTH_SALT }} -e saya_today_web_b2_key_id=${{ secrets.B2_KEY_ID }} -e saya_today_web_b2_application_key=${{ secrets.B2_APPLICATION_KEY }} -e saya_today_web_environment=stage -e saya_today_web_tag=commit-${{ steps.ghss_vars.outputs.sha_short }}
diff --git a/config/config.go b/config/config.go
index 1393dd8..f57b5cd 100644
--- a/config/config.go
+++ b/config/config.go
@@ -15,7 +15,6 @@ type Config struct {
LocalePath string `json:"LocalePath" yaml:"localePath" validate:"required,filepath"`
AvailableLanguages []AvailableLanguageConfig `json:"AvailableLanguages" yaml:"availableLanguages" validate:"required"`
Auth AuthConfig `json:"Auth" yaml:"auth" validate:"required"`
- Mail MailConfig `json:"Mail" yaml:"mail" validate:"required"`
}
type BlogPagesConfig struct {
@@ -61,21 +60,6 @@ type Sqlite3Config struct {
DSN string `json:"DSN" yaml:"dsn" validate:"required"`
}
-type MailConfig struct {
- ClientHost string `json:"ClientHost" yaml:"clientHost" validate:"required"`
- MailHost string `json:"MailHost" yaml:"mailHost" validate:"required"`
- PublicName string `json:"PublicName" yaml:"publicName" validate:"required"`
- MailAddress string `json:"MailAddress" yaml:"mailAddress" validate:"required"`
- Username string `json:"Username" yaml:"username" validate:"required"`
- Password string `json:"Password" yaml:"password" validate:"required"`
- Salt string `json:"Salt" yaml:"salt" validate:"required"`
- Trigger TriggerConfig `json:"Trigger" yaml:"trigger" validate:"required"`
-}
-
-type TriggerConfig struct {
- OnNewPost string `json:"OnNewPost" yaml:"onNewPost" validate:"cron,required"`
-}
-
func LoadConfig(path string, config *Config) error {
fileBytes, err := os.ReadFile(path)
if err != nil {
diff --git a/config/config.local.yaml b/config/config.local.yaml
index e23c808..6d0abf3 100644
--- a/config/config.local.yaml
+++ b/config/config.local.yaml
@@ -32,16 +32,5 @@ auth:
db:
type: sqlite3
config:
- # dsn: 'file:/tmp/auth.db?cache=shared&mode=rwc&_journal_mode=WAL'
- dsn: 'file:/tmp/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2&_journal_mode=WAL'
+ dsn: 'file:/tmp/auth.db?cache=shared&mode=rwc'
salt: '123'
-mail:
- clientHost: '127.0.0.1:3000'
- mailHost: '${MAIL_HOST}'
- publicName: 'LOCAL.SAYA.TODAY'
- mailAddress: '${MAIL_ADDRESS}'
- username: '${MAIL_USERNAME}'
- password: '${MAIL_PASSWORD}'
- salt: '${MAIL_SALT}'
- trigger:
- onNewPost: "* * * * *"
diff --git a/config/config.prod.yaml b/config/config.prod.yaml
index 0bee79d..38981a8 100644
--- a/config/config.prod.yaml
+++ b/config/config.prod.yaml
@@ -32,15 +32,5 @@ auth:
db:
type: sqlite3
config:
- dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2&_journal_mode=WAL'
+ dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2'
salt: '${AUTH_SALT}'
-mail:
- clientHost: '${FQDN}'
- mailHost: '${MAIL_HOST}'
- publicName: 'SAYA.TODAY'
- mailAddress: '${MAIL_ADDRESS}'
- username: '${MAIL_USERNAME}'
- password: '${MAIL_PASSWORD}'
- salt: '${MAIL_SALT}'
- trigger:
- onNewPost: "0/5 * * * *"
diff --git a/config/config.stage.yaml b/config/config.stage.yaml
index 1c15dae..b05edaf 100644
--- a/config/config.stage.yaml
+++ b/config/config.stage.yaml
@@ -32,15 +32,5 @@ auth:
db:
type: sqlite3
config:
- dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2&_journal_mode=WAL'
+ dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2'
salt: '${AUTH_SALT}'
-mail:
- clientHost: '${FQDN}'
- mailHost: '${MAIL_HOST}'
- publicName: 'DEMO.SAYA.TODAY'
- mailAddress: '${MAIL_ADDRESS}'
- username: '${MAIL_USERNAME}'
- password: '${MAIL_PASSWORD}'
- salt: '${MAIL_SALT}'
- trigger:
- onNewPost: "0/3 * * * *"
diff --git a/deploy/inventory.yml b/deploy/inventory.yml
index a1ecf5d..dde952e 100644
--- a/deploy/inventory.yml
+++ b/deploy/inventory.yml
@@ -7,7 +7,6 @@ all:
saya_today_web_name: sayana-web
saya_today_web_hostname: sayana-web
saya_today_web_ipv4_address: 172.16.0.18
- saya_today_web_listen_address: saya.today
stage:
hosts:
@@ -16,4 +15,3 @@ all:
saya_today_web_name: sayana-demo
saya_today_web_hostname: sayana-demo
saya_today_web_ipv4_address: 172.16.0.17
- saya_today_web_listen_address: demo.saya.today
diff --git a/deploy/playbook.yml b/deploy/playbook.yml
index 2516f4e..dfceaf7 100644
--- a/deploy/playbook.yml
+++ b/deploy/playbook.yml
@@ -18,12 +18,6 @@
B2_APPLICATION_KEY: "{{ saya_today_web_b2_application_key }}"
ENVIRONMENT: "{{ saya_today_web_environment }}"
AUTH_SALT: "{{ saya_today_web_auth_salt }}"
- MAIL_HOST: "{{ saya_today_web_mail_host }}"
- MAIL_ADDRESS: "{{ saya_today_web_mail_address }}"
- MAIL_USERNAME: "{{ saya_today_web_mail_username }}"
- MAIL_PASSWORD: "{{ saya_today_web_mail_password }}"
- MAIL_SALT: "{{ saya_today_web_mail_salt }}"
- FQDN: "{{ saya_today_web_listen_address }}"
network_mode: caddy
networks:
- name: caddy
diff --git a/go.mod b/go.mod
index 4ea7ede..e7547b1 100644
--- a/go.mod
+++ b/go.mod
@@ -19,25 +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/robfig/cron/v3 v3.0.1 // indirect
- github.com/stretchr/testify v1.11.1 // indirect
+ github.com/stretchr/testify v1.10.0 // 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
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/sys v0.35.0 // indirect
- golang.org/x/text v0.29.0 // indirect
+ golang.org/x/text v0.28.0 // indirect
)
diff --git a/go.sum b/go.sum
index dd213f1..bbfad9e 100644
--- a/go.sum
+++ b/go.sum
@@ -13,8 +13,6 @@ 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=
@@ -34,8 +32,6 @@ 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=
@@ -55,20 +51,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=
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
-github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
-github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
@@ -82,10 +72,7 @@ golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
-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 d712736..7b745a8 100644
--- a/internal/b2/client.go
+++ b/internal/b2/client.go
@@ -3,7 +3,6 @@ package b2
import (
"context"
"fmt"
- "slices"
"strings"
"time"
@@ -35,7 +34,6 @@ func NewB2Client(cfg *config.B2Config) (*B2Client, error) {
type BlogPage struct {
Link string
FileName string
- Lang string
Metadata *frontmatter.Metadata
}
@@ -76,9 +74,6 @@ 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,
@@ -88,7 +83,7 @@ func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) {
ActionDate: attrs.Info["action-date"],
PublishedTime: publishedTime,
Thumbnail: attrs.Info["thumbnail"],
- Tags: tags,
+ Tags: strings.Split(attrs.Info["tags"], ","),
Geolocation: attrs.Info["geolocation"],
},
})
diff --git a/internal/blogtrigger/blogtrigger.go b/internal/blogtrigger/blogtrigger.go
deleted file mode 100644
index c5b3097..0000000
--- a/internal/blogtrigger/blogtrigger.go
+++ /dev/null
@@ -1,72 +0,0 @@
-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, cron string, 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(cron, 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) Close() error {
- return bts.s.Shutdown()
-}
-
-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/glightbox/html_renderer.go b/internal/glightbox/html_renderer.go
index 7022793..deafb4d 100644
--- a/internal/glightbox/html_renderer.go
+++ b/internal/glightbox/html_renderer.go
@@ -133,7 +133,7 @@ func (r *GLightboxHTMLRenderer) renderGLightbox(w util.BufWriter, source []byte,
}
w.WriteString(fmt.Sprintf(`
-<div class="justify-content-center display-block">
+<div class="justify-content-center display-block m-1">
<hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%%] ml-auto mr-auto">
<div class="grid masonry-grid-%s mx-auto">
<div class="grid-sizer grid-sizer-%s"></div>
@@ -156,7 +156,7 @@ func (r *GLightboxHTMLRenderer) renderGLightbox(w util.BufWriter, source []byte,
function initMasonryLayout_%s(tm) {
clearTimeout(imgLoad_%s_timer);
- imgLoad_%s_timer = setTimeout(() => msnry_%s.layout(), 100);
+ imgLoad_%s_timer = setTimeout(() => msnry_%s.layout(), 300);
}
imgLoad_%s.on('progress', initMasonryLayout_%s);
diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go
deleted file mode 100644
index d6dbd17..0000000
--- a/internal/mailer/mailer.go
+++ /dev/null
@@ -1,544 +0,0 @@
-package mailer
-
-import (
- "crypto/rand"
- "database/sql"
- "encoding/base64"
- "encoding/binary"
- "fmt"
- "html/template"
- "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"
- "github.com/gofiber/fiber/v2"
- "github.com/wneessen/go-mail"
- "golang.org/x/crypto/argon2"
-)
-
-type Mailer struct {
- verificationCodes *ristretto.Cache[uint64, string]
- unsubscribeCodes *ristretto.Cache[uint64, []byte]
- db *sql.DB
- tm *templatemanager.TemplateManager
- mailClient *mail.Client
- clientHost string
- mailAddress string
- publicName string
- salt []byte
-
- lostMailMap map[string]struct {
- Dur time.Duration
- End time.Time
- CodeExpiry time.Time
- }
- lostMailMapMutex sync.RWMutex
-
- hashMap map[string][]byte
- hashMapMutex sync.RWMutex
-
- l map[string]*locale.LocaleConfig
-}
-
-type SubscriptionType int
-
-const (
- All SubscriptionType = iota
- None
- Specific
-)
-
-func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte, localization map[string]*locale.LocaleConfig) (*Mailer, error) {
- verificationCodes, err := ristretto.NewCache(&ristretto.Config[uint64, string]{
- NumCounters: 10000,
- MaxCost: 1 << 20, // 1 MB
- BufferItems: 64,
- TtlTickerDurationInSec: 3600, // 1 hour
- })
- if err != nil {
- return nil, fmt.Errorf("fail to initialize cache for verification codes: %w", err)
- }
-
- unsubscribeCodes, err := ristretto.NewCache(&ristretto.Config[uint64, []byte]{
- NumCounters: 10000,
- MaxCost: 1 << 20, // 1 MB
- BufferItems: 64,
- TtlTickerDurationInSec: 86400, // 1 day
- })
- if err != nil {
- return nil, fmt.Errorf("fail to initialize cache for verification codes: %w", err)
- }
-
- tm, err := templatemanager.NewTemplateManager(templatemanager.TemplateManagerTemplates{
- Name: "new-post",
- Files: []string{"views/layouts/general-mail.html", "views/messages/new-post.html"},
- }, templatemanager.TemplateManagerTemplates{
- Name: "verify-email",
- Files: []string{"views/layouts/general-mail.html", "views/messages/verify-email.html"},
- })
- if err != nil {
- return nil, fmt.Errorf("fail to initialize template manager for message templating: %w", err)
- }
-
- mailClient, err := mail.NewClient(mailHost,
- mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(mail.TLSMandatory),
- mail.WithUsername(username), mail.WithPassword(password),
- )
- if err != nil {
- return nil, fmt.Errorf("fail to initialize mail client: %w", err)
- }
-
- return &Mailer{
- verificationCodes: verificationCodes,
- unsubscribeCodes: unsubscribeCodes,
- db: db,
- clientHost: clientHost,
- tm: tm,
- mailClient: mailClient,
- mailAddress: mailAddress,
- publicName: publicName,
- salt: salt,
- hashMap: make(map[string][]byte),
- lostMailMap: make(map[string]struct {
- Dur time.Duration
- End time.Time
- CodeExpiry time.Time
- }, 0),
- l: localization}, nil
-}
-
-func (m *Mailer) GetHash(id string) []byte {
- m.hashMapMutex.RLock()
- if val, ok := m.hashMap[id]; ok {
- m.hashMapMutex.RUnlock()
- slog.Debug("gave an old hash", slog.String("hash", base64.RawStdEncoding.EncodeToString(val)))
- return val
- }
- m.hashMapMutex.RUnlock()
-
- m.hashMapMutex.Lock()
- defer m.hashMapMutex.Unlock()
-
- if val, ok := m.hashMap[id]; ok {
- slog.Debug("gave a newly generated hash", slog.String("hash", base64.RawStdEncoding.EncodeToString(val)))
- return val
- }
-
- m.hashMap[id] = argon2.IDKey([]byte(id), m.salt, 1, 64*1024, 4, 32)
- slog.Debug("generated hash", slog.String("hash", base64.RawStdEncoding.EncodeToString(m.hashMap[id])))
- return m.hashMap[id]
-}
-
-func (m *Mailer) IsAllowedToRetryVerification(userId string) (retryAllowed bool, whenAllowed time.Time, codeExpiry time.Time) {
- m.lostMailMapMutex.RLock()
- defer m.lostMailMapMutex.RUnlock()
- previous, ok := m.lostMailMap[userId]
- if ok && previous.End.After(time.Now()) {
- return false, previous.End, previous.CodeExpiry
- }
- return true, time.Time{}, previous.CodeExpiry
-}
-
-func (m *Mailer) MailIsTaken(email string) (bool, error) {
- tx, err := m.db.Begin()
- if err != nil {
- return false, fmt.Errorf("failed to initialize transaction with db: %s", err)
- }
-
- slog.Debug("began db transaction", slog.String("method", "MailIsTaken"))
- defer func(tx *sql.Tx) {
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- }
- slog.Debug("ended db transaction", slog.String("method", "MailIsTaken"))
- }(tx)
-
- var rows *sql.Rows
- if rows, err = tx.Query(`SELECT email FROM user_email_table WHERE email=? LIMIT 1;`, email); err != nil {
- tx.Rollback()
-
- return false, fmt.Errorf("failed to query user-email settings in db: %s", err)
- }
- defer rows.Close()
-
- isTaken := rows.Next()
- return isTaken, nil
-}
-
-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)
- }
-
- slog.Debug("began db transaction", slog.String("method", "GetInfo"))
- defer func(tx *sql.Tx) {
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- }
- slog.Debug("ended db transaction", slog.String("method", "GetInfo"))
- }(tx)
-
- var rows *sql.Rows
- 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)
- }
- defer rows.Close()
-
- if !rows.Next() {
- return "", "", nil
- }
-
- if err = rows.Scan(&email, &lang); err != nil {
- return "", "", fmt.Errorf("failed to scan the result from user-email settings query: %s", err)
- }
- return
-}
-
-func (m *Mailer) Unsubscribe(unsubscribeCodeString string) (clientError error, serverError error) {
- unsubscribeCode, err := strconv.ParseUint(unsubscribeCodeString, 16, 64)
- if err != nil {
- return fmt.Errorf("invalid unsubscribe code: %s", err), nil
- }
-
- userId, _ := m.unsubscribeCodes.Get(unsubscribeCode)
- if len(userId) == 0 {
- return fmt.Errorf("invalid unsubscribe code: have no information about it"), nil
- }
-
- if err = m.Subscribe(userId, None); err != nil {
- return nil, fmt.Errorf("failed to unsubscribe: %s", err)
- }
-
- return nil, nil
-}
-
-func (m *Mailer) SendVerificationCode(userId string, address string, lang string) error {
- 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(address); err != nil {
- return fmt.Errorf("failed to set TO address: %w", err)
- }
-
- message.SetMessageID()
- message.SetDate()
- message.SetBulk()
-
- dur := 1 * time.Minute
- m.lostMailMapMutex.Lock()
- if previous, ok := m.lostMailMap[userId]; ok {
- if time.Now().Before(previous.End) {
- m.lostMailMapMutex.Unlock()
- return fmt.Errorf("user is not allowed to send another verification code until %s", previous.End)
- }
- dur = 2 * m.lostMailMap[userId].Dur
- }
- m.lostMailMap[userId] = struct {
- Dur time.Duration
- End time.Time
- CodeExpiry time.Time
- }{Dur: dur, End: time.Now().Add(dur), CodeExpiry: time.Now().Add(time.Hour)}
- m.lostMailMapMutex.Unlock()
-
- verificationCodeBytes := make([]byte, 8)
- rand.Read(verificationCodeBytes)
- verificationCode := binary.LittleEndian.Uint64(verificationCodeBytes)
-
- verificationInfo := fmt.Sprintf("%s.%s", base64.RawStdEncoding.EncodeToString([]byte(userId)), base64.RawStdEncoding.EncodeToString([]byte(address)))
- m.verificationCodes.Set(verificationCode, verificationInfo, int64(len(verificationInfo)+8))
-
- message.Subject(m.l[lang].Mail.VerifyEmail.Subject)
-
- msg, err := m.tm.Render("verify-email", fiber.Map{
- "L": m.l[lang],
- "Lang": lang,
- "VerificationCode": fmt.Sprintf("%X", verificationCode),
- "ClientHost": m.clientHost,
- })
- if err != nil {
- return fmt.Errorf("failed to render message body: %w", err)
- }
-
- message.SetBodyString(mail.TypeTextHTML, string(msg))
- if err := m.mailClient.DialAndSend(message); err != nil {
- return fmt.Errorf("failed to send verification code message: %w", err)
- }
- slog.Debug("verification code message successfully delivered", slog.String("address", address), slog.String("user_id", userId))
- return nil
-}
-
-func (m *Mailer) Verify(verificationCodeEncoded string, lang string) error {
- verificationCode, err := strconv.ParseUint(verificationCodeEncoded, 16, 64)
- if err != nil {
- return fmt.Errorf("failed to decode verification code from 8-byte hex: %w", err)
- }
-
- verificationInfo, _ := m.verificationCodes.Get(verificationCode)
- if verificationInfo == "" {
- return fmt.Errorf("failed to get verification info by its code (might be absent, might be empty)")
- }
-
- verificationSegments := strings.Split(verificationInfo, ".")
- if len(verificationSegments) != 2 {
- m.verificationCodes.Del(verificationCode)
- return fmt.Errorf("invalid format of verification info: expected %d segments, got %d", 2, len(verificationSegments))
- }
-
- userId, err := base64.RawStdEncoding.DecodeString(verificationSegments[0])
- if err != nil {
- m.verificationCodes.Del(verificationCode)
- delete(m.lostMailMap, verificationSegments[0])
- return fmt.Errorf("could not decode user id: %s", err)
- }
-
- address, err := base64.RawStdEncoding.DecodeString(verificationSegments[1])
- if err != nil {
- m.verificationCodes.Del(verificationCode)
- delete(m.lostMailMap, verificationSegments[0])
- return fmt.Errorf("could not decode address: %s", err)
- }
-
- tx, err := m.db.Begin()
- if err != nil {
- return fmt.Errorf("failed to initialize transaction with db: %s", err)
- }
-
- slog.Debug("began db transaction", slog.String("method", "Verify"))
- if _, err = tx.Exec(`INSERT INTO user_email_table(user_id, email, lang) VALUES(?, ?, ?)
- ON CONFLICT(user_id) DO UPDATE SET
- email=excluded.email,
- lang=excluded.lang;`, m.GetHash(string(userId)), address, lang); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "Verify"))
- return fmt.Errorf("failed to configure user-email settings in db: %s", err)
- }
-
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "Verify"))
- return fmt.Errorf("failed to commit transaction to db: %s", err)
- }
- slog.Debug("ended db transaction", slog.String("method", "Verify"))
-
- m.verificationCodes.Del(verificationCode)
- delete(m.lostMailMap, verificationSegments[0])
- return nil
-}
-
-func (m *Mailer) GetSubscriptions(userId string) (subscriptionType SubscriptionType, tags []string, err error) {
- tx, err := m.db.Begin()
- if err != nil {
- return None, nil, fmt.Errorf("failed to initialize transaction with db: %s", err)
- }
-
- slog.Debug("began db transaction", slog.String("method", "GetSubscriptions"))
- defer func(tx *sql.Tx) {
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- }
- slog.Debug("ended db transaction", slog.String("method", "GetSubscriptions"))
- }(tx)
-
- hash := m.GetHash(userId)
-
- var rows *sql.Rows
- if rows, err = tx.Query(`SELECT tags FROM subscription_user_to_tags_table WHERE user_id=? LIMIT 1;`, hash); err != nil {
- tx.Rollback()
- return None, nil, fmt.Errorf("failed to query user-to-tags table in db for the user: %s", err)
- }
- defer rows.Close()
-
- if !rows.Next() {
- return None, nil, nil
- }
-
- tagsString := ""
- if err = rows.Scan(&tagsString); err != nil {
- return None, nil, fmt.Errorf("failed to scan the result from user-to-tags query: %s", err)
- }
-
- switch tagsString {
- case "":
- return None, nil, nil
- case "_all":
- return All, nil, nil
- default:
- return Specific, strings.Split(tagsString, ","), nil
- }
-}
-
-func (m *Mailer) Subscribe(userIdHash []byte, subscriptionType SubscriptionType, tags ...string) error {
- tx, err := m.db.Begin()
- if err != nil {
- return fmt.Errorf("failed to initialize transaction with db: %s", err)
- }
-
- slog.Debug("began db transaction", slog.String("method", "Subscribe"))
-
- slices.Sort(tags)
- tagsOutput := ""
- switch subscriptionType {
- case All:
- tagsOutput = "_all"
- case None:
- tagsOutput = ""
- case Specific:
- tagsOutput = strings.Join(tags, ",")
- }
-
- if _, err = tx.Exec(`INSERT INTO subscription_user_to_tags_table(user_id, tags) VALUES(?, ?)
- ON CONFLICT(user_id) DO UPDATE SET
- tags=excluded.tags;`, userIdHash, tagsOutput); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "Subscribe"))
- return fmt.Errorf("failed to configure user-to-tags table in db for the user: %s", err)
- }
-
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "Subscribe"))
- return fmt.Errorf("failed to commit transaction to db: %s", err)
- }
- slog.Debug("ended db transaction", slog.String("method", "Subscribe"))
-
- return nil
-}
-
-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)
- }
-
- slog.Debug("began db transaction", slog.String("method", "NewPost"))
- var rows *sql.Rows
- if rows, err = tx.Query(`SELECT user_id, tags FROM subscription_user_to_tags_table;`); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "NewPost"))
- return fmt.Errorf("failed to query user-to-tags table in db: %s", err)
- }
-
- var userId []byte
- usersToSend := make([]struct {
- userId []byte
- email 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
- }
-
- if tagsString == "_all" {
- usersToSend = append(usersToSend, struct {
- userId []byte
- email string
- }{userId, ""})
- continue
- }
-
- pageTags := strings.Split(tagsString, ",")
- for _, tag := range pageTags {
- if _, found := slices.BinarySearch(post.Metadata.Tags, tag); found {
- usersToSend = append(usersToSend, struct {
- userId []byte
- email string
- }{userId, ""})
- continue rowLoop
- }
- }
- }
-
- tx.Commit()
- rows.Close()
- slog.Debug("ended db transaction", slog.String("method", "NewPost"))
-
- for i := range usersToSend {
- email, lang, err := m.GetInfo(usersToSend[i].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
- }
-
- usersToSend[i].email = email
- }
-
- messages := make([]*mail.Msg, 0, len(usersToSend))
- for _, user := range usersToSend {
- if user.email == "" {
- continue
- }
- unsubscribeCodeBytes := make([]byte, 8)
- rand.Read(unsubscribeCodeBytes)
- unsubscribeCode := binary.LittleEndian.Uint64(unsubscribeCodeBytes)
-
- unsubscribeFooter := strings.Replace(m.l[post.Lang].Mail.UnsubscribeFooter, "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1)
- unsubscribeFooter = strings.Replace(unsubscribeFooter, "{/}", "</a>", 1)
-
- msgBody, err := m.tm.Render("new-post", fiber.Map{
- "L": m.l[post.Lang],
- "Lang": post.Lang,
- "Post": post,
- "ClientHost": m.clientHost,
- "UnsubscribeFooter": template.HTML(unsubscribeFooter),
- })
-
- 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.email); 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))
-
- m.unsubscribeCodes.Set(unsubscribeCode, user.userId, 40)
-
- 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-blog-search.go b/internal/router/api-v1-blog-search.go
index 5b1d14d..ab5425c 100644
--- a/internal/router/api-v1-blog-search.go
+++ b/internal/router/api-v1-blog-search.go
@@ -17,7 +17,7 @@ import (
)
func init() {
- assert(0, tm.Add("catalogue-blog-cards", "views/partials/catalogue-blog-cards.html", "views/partials/catalogue-blog-card-tags.html"))
+ tm.Add("catalogue-blog-cards", "views/partials/catalogue-blog-cards.html", "views/partials/catalogue-blog-card-tags.html")
}
func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
diff --git a/internal/router/api-v1-email-is-in-verification.go b/internal/router/api-v1-email-is-in-verification.go
deleted file mode 100644
index 83c783c..0000000
--- a/internal/router/api-v1-email-is-in-verification.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package router
-
-import (
- "fmt"
- "net/url"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("personal-page-status", "views/partials/personal-page-status.html"))
-}
-
-func Api_V1_Email_IsInVerification(l map[string]*locale.LocaleConfig) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
-
- id := c.IP()
- lang := "en"
-
- var referer, path string
- var pathParts []string
- var urlStruct *url.URL
- var err error
-
- referer = c.Get("Referer", "")
- if referer == "" {
- goto skipFetchingLang
- }
-
- urlStruct, err = url.ParseRequestURI(referer)
- if err != nil {
- goto skipFetchingLang
- }
-
- path = urlStruct.EscapedPath()
- pathParts = strings.Split(strings.Trim(path, "/"), "/")
- if len(pathParts) == 0 {
- goto skipFetchingLang
- }
-
- lang = pathParts[0]
-
- skipFetchingLang:
- isAllowed, whenAllowed, codeExpiry := Mailer.IsAllowedToRetryVerification(id)
- if !isAllowed {
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Neutral", fiber.StatusOK, lang, strings.ReplaceAll(l[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")), map[string]string{
- "striked-end-time": fmt.Sprint(whenAllowed.UnixMilli()),
- "code-expiry-time": fmt.Sprint(codeExpiry.UnixMilli()),
- })
- }
- return api_v1_email_sendStatusHtml(c, "email-message", l, "OK", fiber.StatusOK, lang, "", map[string]string{
- "code-expiry-time": fmt.Sprint(codeExpiry.UnixMilli()),
- })
- }
-}
diff --git a/internal/router/api-v1-email-send-verification-code.go b/internal/router/api-v1-email-send-verification-code.go
deleted file mode 100644
index 9372eb3..0000000
--- a/internal/router/api-v1-email-send-verification-code.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package router
-
-import (
- "fmt"
- "html/template"
- "log/slog"
- "net/url"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("personal-page-status", "views/partials/personal-page-status.html"))
-}
-
-func Api_V1_Email_SendVerificationCode(l map[string]*locale.LocaleConfig) 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) != 2 {
- return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/user'")
- }
-
- lang := pathParts[0]
- id := c.IP()
-
- email := c.FormValue("email")
- if email == "" {
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.EmailEmpty, map[string]string{})
- }
-
- isTaken, err := Mailer.MailIsTaken(email)
- if err != nil {
- slog.Error("failed to check if address is already taken", slog.String("error", err.Error()))
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.VerificationCodeSendingError, map[string]string{})
- }
- if isTaken {
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.EmailTaken, map[string]string{})
- }
-
- if isAllowed, whenAllowed, _ := Mailer.IsAllowedToRetryVerification(id); !isAllowed {
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, strings.ReplaceAll(l[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")), map[string]string{
- "striked-end-time": fmt.Sprint(whenAllowed.UnixMilli()),
- })
- }
-
- 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{})
- }
-
- if err = Mailer.SendVerificationCode(id, email, lang); err != nil {
- slog.Error("failed to send a verification code", slog.String("error", err.Error()))
- return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.VerificationCodeSendingError, map[string]string{})
- }
-
- _, endTime, codeExpiry := Mailer.IsAllowedToRetryVerification(id)
- return api_v1_email_sendStatusHtml(c, "email-message", l, "OK", fiber.StatusOK, lang, strings.ReplaceAll(l[lang].UserProfile.VerificationCodeSent, "{}", endTime.Format("2006-01-02 15:04:05 MST")), map[string]string{
- "striked-end-time": fmt.Sprint(endTime.UnixMilli()),
- "code-expiry-time": fmt.Sprint(codeExpiry.UnixMilli()),
- })
- }
-}
-
-func api_v1_email_sendStatusHtml(c *fiber.Ctx, divId string, l map[string]*locale.LocaleConfig, status string, code int, lang string, message string, dataAttributes map[string]string) error {
- sterileDataset := make(map[string]interface{})
- for k, v := range dataAttributes {
- sterileDataset[k] = template.HTMLAttr(fmt.Sprintf("data-%s=\"%s\"", k, template.HTMLEscapeString(v)))
- }
-
- content, err := tm.Render("personal-page-status", fiber.Map{
- "L": l[lang],
- "Status": status,
- "Message": message,
- "StatusId": divId,
- "DataAttributes": sterileDataset,
- })
- if err != nil {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- slog.Error("failed to render the email status message", slog.String("error", err.Error()), slog.String("div_id", divId), slog.String("message", message))
- return c.Status(fiber.ErrInternalServerError.Code).SendString("Failed to render the email status message, please ask administrator for a more detailed cause")
- }
- c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
- return c.Status(code).Send(content)
-}
diff --git a/internal/router/api-v1-email-verify.go b/internal/router/api-v1-email-verify.go
deleted file mode 100644
index 7fbfdec..0000000
--- a/internal/router/api-v1-email-verify.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package router
-
-import (
- "fmt"
- "log/slog"
- "net/url"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("personal-page-status", "views/partials/personal-page-status.html"))
-}
-
-func Api_V1_Email_Verify(l map[string]*locale.LocaleConfig) 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) != 2 {
- return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/user'")
- }
-
- lang := pathParts[0]
-
- verificationCode := c.FormValue("email_code")
- if verificationCode == "" {
- return api_v1_email_sendStatusHtml(c, "verification-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.VerificationEmpty, map[string]string{})
- }
-
- if err = Mailer.Verify(verificationCode, lang); err != nil {
- slog.Error("verification code is invalid", slog.String("verification_code", verificationCode), slog.String("error", err.Error()))
- return api_v1_email_sendStatusHtml(c, "verification-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.VerificationFailed, map[string]string{})
- }
- return api_v1_email_sendStatusHtml(c, "verification-message", l, "OK", fiber.StatusOK, lang, l[lang].UserProfile.VerificationSuccess+"\n\n"+l[lang].UserProfile.RefreshPage, map[string]string{
- "hide-verification-panel": "true",
- })
- }
-}
diff --git a/internal/router/api-v1-general-page-body.go b/internal/router/api-v1-general-page-body.go
index b5567b7..fe21957 100644
--- a/internal/router/api-v1-general-page-body.go
+++ b/internal/router/api-v1-general-page-body.go
@@ -1,7 +1,6 @@
package router
import (
- "bytes"
"fmt"
"html/template"
"log/slog"
@@ -14,21 +13,16 @@ 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"
"github.com/SayaAndy/saya-today-web/locale"
"github.com/gofiber/fiber/v2"
"github.com/yuin/goldmark"
)
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"))
+ tm.Add("general-page-body", "views/partials/general-page-body.html")
}
func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, b2Client *b2.B2Client, md goldmark.Markdown) func(c *fiber.Ctx) error {
@@ -45,16 +39,15 @@ func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []config.A
}
path := urlStruct.EscapedPath()
- trimmedPath := strings.Trim(path, "/")
- cacheKey := fmt.Sprintf("body.%s", trimmedPath)
- if val, ok := PCache.Get(cacheKey); !strings.HasSuffix(trimmedPath, "user") && val != nil && ok {
+ cacheKey := fmt.Sprintf("body.%s", path)
+ if val, ok := PCache.Get(cacheKey); val != nil && ok {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
return c.Status(fiber.StatusOK).Type("html").Send(val)
}
lang := ""
- pathParts := strings.Split(trimmedPath, "/")
+ pathParts := strings.Split(strings.Trim(path, "/"), "/")
if len(pathParts) == 1 && pathParts[0] == "" {
pathParts = []string{}
}
@@ -97,54 +90,39 @@ func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []config.A
queryTags = append(queryTags, string(match[1]))
}
- tagsArray, err := getTags(b2Client, lang)
+ pages, err := b2Client.Scan(lang + "/")
if err != nil {
- slog.Warn("failed to get the available tags", slog.String("error", err.Error()))
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to gather available tags")
+ slog.Warn("failed to scan pages via b2", slog.String("error", err.Error()))
+ return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages via b2: %s", slog.String("error", err.Error())))
}
- values["Tags"] = tagsArray
- values["QuerySort"] = querySort
- values["QueryTags"] = strings.Join(queryTags, ",")
- values["Title"] = l[lang].BlogSearch.Header
-
- additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html")
- } else if len(pathParts) == 2 && pathParts[1] == "user" {
- values["Title"] = l[lang].UserProfile.Header
-
- 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()))
- }
-
- tagsArray, err := getTags(b2Client, lang)
- if err != nil {
- slog.Warn("failed to get the available tags", slog.String("error", err.Error()))
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to gather available tags")
+ tagsMap := make(map[string]int)
+ for _, page := range pages {
+ for _, tag := range page.Metadata.Tags {
+ tagsMap[tag]++
+ }
}
+ slog.Debug("enlist pages for catalogue", slog.Int("tag_count", len(tagsMap)), slog.Int("page_count", len(pages)), slog.String("path", c.Path()))
- subscriptionType, tags, err := Mailer.GetSubscriptions(c.IP())
- if err != nil {
- slog.Warn("failed to get the user subscriptions", slog.String("error", err.Error()))
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to get the user subscriptions")
+ type Tag struct {
+ Name string `json:"Name" yaml:"name"`
+ Count int `json:"Count" yaml:"count"`
}
- switch subscriptionType {
- case mailer.None:
- values["TagsPicked"] = "none"
- case mailer.All:
- values["TagsPicked"] = "all"
- case mailer.Specific:
- values["TagsPicked"] = "specific"
+ tagsArray := make([]Tag, 0, len(tagsMap))
+ for tag, count := range tagsMap {
+ tagsArray = append(tagsArray, Tag{tag, count})
}
+ slices.SortFunc(tagsArray, func(a Tag, b Tag) int {
+ return strings.Compare(a.Name, b.Name)
+ })
- values["TagsPickedList"] = tags
-
- values["Email"] = email
- values["EmailCode"] = c.Query("email_code")
- values["ExistingTags"] = tagsArray
+ values["Tags"] = tagsArray
+ values["QuerySort"] = querySort
+ values["QueryTags"] = strings.Join(queryTags, ",")
+ values["Title"] = l[lang].BlogSearch.Header
- additionalTemplates = append(additionalTemplates, "views/pages/user-page.html")
+ additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html")
} else if len(pathParts) == 3 && pathParts[1] == "blog" {
metadata, parsedMarkdown, err := readBlogPost(md, b2Client, lang+"/"+pathParts[2])
if err != nil {
@@ -196,48 +174,3 @@ func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []config.A
return c.Status(fiber.StatusOK).Type("html").Send(content)
}
}
-
-func readBlogPost(md goldmark.Markdown, b2Client *b2.B2Client, sourceName string) (metadata *frontmatter.Metadata, html string, err error) {
- metadata, markdown, err := b2Client.ReadFrontmatter(sourceName + ".md")
- if err != nil {
- return nil, "", fmt.Errorf("failed to read a frontmatter file: %w", err)
- }
-
- var buf bytes.Buffer
- if err := md.Convert(markdown, &buf); err != nil {
- return nil, "", fmt.Errorf("convert source context from md to html: %w", err)
- }
-
- return metadata, buf.String(), nil
-}
-
-type Tag struct {
- Name string `json:"Name" yaml:"name"`
- Count int `json:"Count" yaml:"count"`
-}
-
-func getTags(b2Client *b2.B2Client, lang string) (tags []Tag, err error) {
- pages, err := b2Client.Scan(lang + "/")
- if err != nil {
- slog.Warn("failed to scan pages via b2", slog.String("error", err.Error()))
- return nil, fmt.Errorf("failed to scan pages via b2: %w", err)
- }
-
- tagsMap := make(map[string]int)
- for _, page := range pages {
- for _, tag := range page.Metadata.Tags {
- tagsMap[tag]++
- }
- }
- slog.Debug("enlist pages for catalogue", slog.Int("tag_count", len(tagsMap)), slog.Int("page_count", len(pages)), slog.String("lang", lang))
-
- tagsArray := make([]Tag, 0, len(tagsMap))
- for tag, count := range tagsMap {
- tagsArray = append(tagsArray, Tag{tag, count})
- }
- slices.SortFunc(tagsArray, func(a Tag, b Tag) int {
- return strings.Compare(a.Name, b.Name)
- })
-
- return tagsArray, nil
-}
diff --git a/internal/router/api-v1-general-page-bottom-embeds.go b/internal/router/api-v1-general-page-bottom-embeds.go
index b408187..0a2030f 100644
--- a/internal/router/api-v1-general-page-bottom-embeds.go
+++ b/internal/router/api-v1-general-page-bottom-embeds.go
@@ -14,7 +14,7 @@ import (
)
func init() {
- assert(0, tm.Add("general-page-bottom-embeds", "views/partials/general-page-bottom-embeds.html"))
+ tm.Add("general-page-bottom-embeds", "views/partials/general-page-bottom-embeds.html")
}
func Api_V1_GeneralPage_BottomEmbeds(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
@@ -63,8 +63,6 @@ func Api_V1_GeneralPage_BottomEmbeds(l map[string]*locale.LocaleConfig, langs []
if len(pathParts) == 2 && pathParts[1] == "blog" {
additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html")
- } else if len(pathParts) == 2 && pathParts[1] == "user" {
- additionalTemplates = append(additionalTemplates, "views/pages/user-page.html")
} else if len(pathParts) == 3 && pathParts[1] == "blog" {
additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html")
} else if len(pathParts) == 1 {
diff --git a/internal/router/api-v1-general-page-footer.go b/internal/router/api-v1-general-page-footer.go
index 99d899a..2030b3b 100644
--- a/internal/router/api-v1-general-page-footer.go
+++ b/internal/router/api-v1-general-page-footer.go
@@ -13,7 +13,7 @@ import (
)
func init() {
- assert(0, tm.Add("general-page-footer", "views/partials/general-page-footer.html"))
+ tm.Add("general-page-footer", "views/partials/general-page-footer.html")
}
func Api_V1_GeneralPage_Footer(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig) func(c *fiber.Ctx) error {
@@ -63,8 +63,6 @@ func Api_V1_GeneralPage_Footer(l map[string]*locale.LocaleConfig, langs []config
if len(pathParts) == 2 && pathParts[1] == "blog" {
additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html")
- } else if len(pathParts) == 2 && pathParts[1] == "user" {
- additionalTemplates = append(additionalTemplates, "views/pages/user-page.html")
} else if len(pathParts) == 3 && pathParts[1] == "blog" {
additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html")
} else if len(pathParts) == 1 {
diff --git a/internal/router/api-v1-general-page-header.go b/internal/router/api-v1-general-page-header.go
index 768417d..a2c832b 100644
--- a/internal/router/api-v1-general-page-header.go
+++ b/internal/router/api-v1-general-page-header.go
@@ -14,7 +14,7 @@ import (
)
func init() {
- assert(0, tm.Add("general-page-header", "views/partials/general-page-header.html"))
+ tm.Add("general-page-header", "views/partials/general-page-header.html")
}
func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
@@ -65,9 +65,6 @@ func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []config
if len(pathParts) == 2 && pathParts[1] == "blog" {
values["Title"] = l[lang].BlogSearch.Header
additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html")
- } else if len(pathParts) == 2 && pathParts[1] == "user" {
- values["Title"] = l[lang].UserProfile.Header
- additionalTemplates = append(additionalTemplates, "views/pages/user-page.html")
} else if len(pathParts) == 3 && pathParts[1] == "blog" {
metadata, _, err := b2Client.ReadFrontmatter(lang + "/" + pathParts[2] + ".md")
if err != nil {
diff --git a/internal/router/api-v1-general-page-top-embeds.go b/internal/router/api-v1-general-page-top-embeds.go
index f3e480b..544e3a4 100644
--- a/internal/router/api-v1-general-page-top-embeds.go
+++ b/internal/router/api-v1-general-page-top-embeds.go
@@ -13,7 +13,7 @@ import (
)
func init() {
- assert(0, tm.Add("general-page-top-embeds", "views/partials/general-page-top-embeds.html"))
+ tm.Add("general-page-top-embeds", "views/partials/general-page-top-embeds.html")
}
func Api_V1_GeneralPage_TopEmbeds(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig) func(c *fiber.Ctx) error {
@@ -63,8 +63,6 @@ func Api_V1_GeneralPage_TopEmbeds(l map[string]*locale.LocaleConfig, langs []con
if len(pathParts) == 2 && pathParts[1] == "blog" {
additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html")
- } else if len(pathParts) == 2 && pathParts[1] == "user" {
- additionalTemplates = append(additionalTemplates, "views/pages/user-page.html")
} else if len(pathParts) == 3 && pathParts[1] == "blog" {
additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html")
} else if len(pathParts) == 1 {
diff --git a/internal/router/api-v1-general-page.go b/internal/router/api-v1-general-page.go
index c8b4362..a77fc5e 100644
--- a/internal/router/api-v1-general-page.go
+++ b/internal/router/api-v1-general-page.go
@@ -1,7 +1,6 @@
package router
import (
- "fmt"
"log/slog"
"strings"
@@ -11,7 +10,7 @@ import (
)
func init() {
- assert(0, tm.Add("general-page", "views/layouts/general-page.html"))
+ tm.Add("general-page", "views/layouts/general-page.html")
}
func Api_V1_GeneralPage(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig) func(c *fiber.Ctx) error {
@@ -35,8 +34,7 @@ func Api_V1_GeneralPage(l map[string]*locale.LocaleConfig, langs []config.Availa
}
langIsAvailable:
- queryString := string(c.Request().URI().QueryString())
- cacheKey := fmt.Sprintf("general-page.%s?%s", lang, queryString)
+ cacheKey := "general-page." + lang
if val, ok := PCache.Get(cacheKey); val != nil && ok {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
return c.Status(fiber.StatusOK).Type("html").Send(val)
@@ -45,7 +43,7 @@ func Api_V1_GeneralPage(l map[string]*locale.LocaleConfig, langs []config.Availa
content, err := tm.Render("general-page", fiber.Map{
"L": l[lang],
"Lang": lang,
- "QueryString": queryString,
+ "QueryString": string(c.Request().URI().QueryString()),
})
if err != nil {
slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error()))
diff --git a/internal/router/api-v1-like.go b/internal/router/api-v1-like.go
index 323c457..327dafa 100644
--- a/internal/router/api-v1-like.go
+++ b/internal/router/api-v1-like.go
@@ -13,7 +13,7 @@ import (
)
func init() {
- assert(0, tm.Add("blog-page-like-button", "views/partials/blog-page-like-button.html"))
+ tm.Add("blog-page-like-button", "views/partials/blog-page-like-button.html")
}
func Api_V1_Like_Put(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c *fiber.Ctx) error {
diff --git a/internal/router/api-v1-subs.go b/internal/router/api-v1-subs.go
deleted file mode 100644
index 72f16e0..0000000
--- a/internal/router/api-v1-subs.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package router
-
-import (
- "net/url"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/internal/mailer"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("personal-page-status", "views/partials/personal-page-status.html"))
-}
-
-func Api_V1_Subs_Put(l map[string]*locale.LocaleConfig) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
-
- id := c.IP()
- lang := "en"
-
- var referer, path string
- var pathParts []string
- var urlStruct *url.URL
- var err error
-
- referer = c.Get("Referer", "")
- if referer == "" {
- goto skipFetchingLang
- }
-
- urlStruct, err = url.ParseRequestURI(referer)
- if err != nil {
- goto skipFetchingLang
- }
-
- path = urlStruct.EscapedPath()
- pathParts = strings.Split(strings.Trim(path, "/"), "/")
- if len(pathParts) == 0 {
- goto skipFetchingLang
- }
-
- lang = pathParts[0]
-
- skipFetchingLang:
- subscriptionType := c.FormValue("tags")
- var subscriptionTypeEnum mailer.SubscriptionType
- switch subscriptionType {
- case "all":
- subscriptionTypeEnum = mailer.All
- case "none":
- subscriptionTypeEnum = mailer.None
- case "specific":
- subscriptionTypeEnum = mailer.Specific
- default:
- return api_v1_email_sendStatusHtml(c, "subs-message", l, "Failed", fiber.StatusUnprocessableEntity, lang, l[lang].UserProfile.SubscribeInvalidType, map[string]string{})
- }
-
- specificTags := c.FormValue("tags_picked")
- if err = Mailer.Subscribe(Mailer.GetHash(id), subscriptionTypeEnum, specificTags); err != nil {
- return api_v1_email_sendStatusHtml(c, "subs-message", l, "Failed", fiber.StatusUnprocessableEntity, lang, l[lang].UserProfile.FailedToSubscribe, map[string]string{})
- }
-
- return api_v1_email_sendStatusHtml(c, "subs-message", l, "OK", fiber.StatusOK, lang, l[lang].UserProfile.SubscribedSuccessfully, map[string]string{})
- }
-}
diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go
index e7cda30..d607cdd 100644
--- a/internal/router/client-cache.go
+++ b/internal/router/client-cache.go
@@ -31,12 +31,10 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) {
if err != nil {
return nil, fmt.Errorf("fail to init transaction with db to fill cache: %w", err)
}
- slog.Debug("began db transaction", slog.String("method", "NewClientCache"))
rows, err := tx.Query("select * from blog_likes;")
if err != nil {
tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
return nil, fmt.Errorf("fail to query db for blog_likes to fill cache: %w", err)
}
@@ -64,7 +62,6 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) {
rows, err = tx.Query("select * from blog_views;")
if err != nil {
tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
return nil, fmt.Errorf("fail to query db for blog_views to fill cache: %w", err)
}
@@ -84,11 +81,8 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) {
}
if err = tx.Commit(); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
return nil, fmt.Errorf("fail to commit transaction in db: %w", err)
}
- slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
return &ClientCache{
hashMap: make(map[string]string),
@@ -105,28 +99,18 @@ func (c *ClientCache) Close() error {
if err != nil {
return fmt.Errorf("fail to init transaction with db to dump cache: %w", err)
}
- slog.Debug("began db transaction in ClientCache.Close")
if err = batchSave(tx, "blog_likes", c.likePageMap); err != nil {
tx.Rollback()
- slog.Debug("ended db transaction in ClientCache.Close")
return fmt.Errorf("fail to save blog_likes: %s", err)
}
if err = batchSave(tx, "blog_views", c.viewPageMap); err != nil {
tx.Rollback()
- slog.Debug("ended db transaction in ClientCache.Close")
return fmt.Errorf("fail to save blog_views: %s", err)
}
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction in ClientCache.Close")
- return fmt.Errorf("fail to commit all the changes related to cache: %s", err)
- }
-
- slog.Debug("ended db transaction in ClientCache.Close")
- return nil
+ return tx.Commit()
}
func (c *ClientCache) GetHash(id string) string {
diff --git a/internal/router/lang-blog-title.go b/internal/router/lang-blog-title.go
new file mode 100644
index 0000000..6e80671
--- /dev/null
+++ b/internal/router/lang-blog-title.go
@@ -0,0 +1,86 @@
+package router
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "log/slog"
+ "strconv"
+ "strings"
+
+ "github.com/SayaAndy/saya-today-web/config"
+ "github.com/SayaAndy/saya-today-web/internal/b2"
+ "github.com/SayaAndy/saya-today-web/internal/frontmatter"
+ "github.com/SayaAndy/saya-today-web/locale"
+ "github.com/gofiber/fiber/v2"
+ "github.com/yuin/goldmark"
+)
+
+func init() {
+ tm.Add("blog-page", "views/layouts/general-page.html", "views/pages/blog-page.html")
+}
+
+func Lang_Blog_Title(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, b2Client *b2.B2Client, md goldmark.Markdown) func(c *fiber.Ctx) error {
+ return func(c *fiber.Ctx) error {
+ ip := c.IP()
+ slog.Debug("client entering blog page", slog.String("ip", ip), slog.String("page", c.Path()))
+
+ lang := c.Params("lang")
+ for _, availableLang := range langs {
+ if availableLang.Name == lang {
+ goto langIsAvailable
+ }
+ }
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang))
+
+ langIsAvailable:
+ metadata, parsedMarkdown, err := readBlogPost(md, b2Client, lang+"/"+c.Params("title"))
+ if err != nil {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("failed to find '%s' post", c.Params("title")))
+ }
+
+ geolocationParts := strings.Split(metadata.Geolocation, " ")
+ var x, y, areaError string
+ if len(geolocationParts) >= 2 {
+ x = geolocationParts[0]
+ y = geolocationParts[1]
+ }
+ if len(geolocationParts) >= 3 {
+ areaError = geolocationParts[2]
+ }
+
+ content, err := tm.Render("blog-page", fiber.Map{
+ "Title": metadata.Title,
+ "PublishedDate": metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00"),
+ "PublishedYear": strconv.Itoa(metadata.PublishedTime.Year()),
+ "ParsedMarkdown": template.HTML(parsedMarkdown),
+ "MapLocationX": x,
+ "MapLocationY": y,
+ "MapLocationAreaMeters": areaError,
+ "Lang": lang,
+ "QueryString": string(c.Request().URI().QueryString()),
+ })
+ if err != nil {
+ slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog/"+c.Params("title")), slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
+ }
+
+ return c.Type("html").Send(content)
+ }
+}
+
+func readBlogPost(md goldmark.Markdown, b2Client *b2.B2Client, sourceName string) (metadata *frontmatter.Metadata, html string, err error) {
+ metadata, markdown, err := b2Client.ReadFrontmatter(sourceName + ".md")
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to read a frontmatter file: %w", err)
+ }
+
+ var buf bytes.Buffer
+ if err := md.Convert(markdown, &buf); err != nil {
+ return nil, "", fmt.Errorf("convert source context from md to html: %w", err)
+ }
+
+ return metadata, buf.String(), nil
+}
diff --git a/internal/router/lang-blog.go b/internal/router/lang-blog.go
new file mode 100644
index 0000000..960ac3d
--- /dev/null
+++ b/internal/router/lang-blog.go
@@ -0,0 +1,98 @@
+package router
+
+import (
+ "fmt"
+ "log/slog"
+ "net/url"
+ "regexp"
+ "slices"
+ "strings"
+
+ "github.com/SayaAndy/saya-today-web/config"
+ "github.com/SayaAndy/saya-today-web/internal/b2"
+ "github.com/SayaAndy/saya-today-web/locale"
+ "github.com/gofiber/fiber/v2"
+)
+
+func init() {
+ tm.Add("blog-catalogue", "views/layouts/general-page.html", "views/pages/blog-catalogue.html")
+}
+
+func Lang_Blog(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
+ return func(c *fiber.Ctx) error {
+ lang := c.Params("lang")
+ for _, availableLang := range langs {
+ if availableLang.Name == lang {
+ goto langIsAvailable
+ }
+ }
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang))
+
+ langIsAvailable:
+ querySort := c.Query("sort")
+ if querySort == "" {
+ querySort = "publicationDateDesc"
+ }
+
+ encodedQuery := c.Request().URI().QueryString()
+ re, err := regexp.Compile(`tags\[\]=([\w]+)`)
+ if err != nil {
+ slog.Warn("failed to generate regex for tags gathering", slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate regex for tags gathering")
+ }
+ decodedQuery, _ := url.QueryUnescape(string(encodedQuery))
+ matches := re.FindAllStringSubmatch(decodedQuery, -1)
+
+ queryTags := make([]string, 0, len(matches))
+ for _, match := range matches {
+ queryTags = append(queryTags, string(match[1]))
+ }
+
+ pages, err := b2Client.Scan(lang + "/")
+ status := fiber.StatusOK
+ if err != nil {
+ status = fiber.StatusPartialContent
+ pages = []*b2.BlogPage{}
+ }
+
+ tagsMap := make(map[string]int)
+ for _, page := range pages {
+ for _, tag := range page.Metadata.Tags {
+ tagsMap[tag]++
+ }
+ }
+ slog.Debug("enlist pages for catalogue", slog.Int("tag_count", len(tagsMap)), slog.Int("page_count", len(pages)), slog.String("path", c.Path()))
+
+ type Tag struct {
+ Name string `json:"Name" yaml:"name"`
+ Count int `json:"Count" yaml:"count"`
+ }
+
+ tagsArray := make([]Tag, 0, len(tagsMap))
+ for tag, count := range tagsMap {
+ tagsArray = append(tagsArray, Tag{tag, count})
+ }
+ slices.SortFunc(tagsArray, func(a Tag, b Tag) int {
+ return strings.Compare(a.Name, b.Name)
+ })
+
+ content, err := tm.Render("blog-catalogue", fiber.Map{
+ "QuerySort": querySort,
+ "QueryTags": strings.Join(queryTags, ","),
+ "QueryString": string(c.Request().URI().QueryString()),
+ "Tags": tagsArray,
+ "Lang": lang,
+ "L": l[lang],
+ "PublishedYear": "2025",
+ "Title": l[lang].BlogSearch.Header,
+ })
+ if err != nil {
+ 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")
+ }
+
+ return c.Type("html").Status(status).Send(content)
+ }
+}
diff --git a/internal/router/lang-map.go b/internal/router/lang-map.go
index 619b05b..a744f02 100644
--- a/internal/router/lang-map.go
+++ b/internal/router/lang-map.go
@@ -13,7 +13,7 @@ import (
)
func init() {
- assert(0, tm.Add("global-map", "views/pages/global-map.html"))
+ tm.Add("global-map", "views/pages/global-map.html")
}
func Lang_Map(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
diff --git a/internal/router/lang-user-unsubscribe.go b/internal/router/lang-user-unsubscribe.go
deleted file mode 100644
index c39f5e7..0000000
--- a/internal/router/lang-user-unsubscribe.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package router
-
-import (
- "fmt"
- "html/template"
- "log/slog"
-
- "github.com/SayaAndy/saya-today-web/config"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("unsubscribe-page", "views/pages/unsubscribe-page.html"))
-}
-
-func Lang_User_Unsubscribe(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- lang := c.Params("lang")
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
- }
- return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang))
-
- langIsAvailable:
- var statusEmoji, statusText, statusColor string
- var status int
-
- unsubscribeCode := c.FormValue("code")
- if unsubscribeCode == "" {
- statusColor = "0, 0, 255"
- statusEmoji = "(╭ರ_•́)"
- statusText = l[lang].UnsubscribePage.UnsetCode
- status = fiber.ErrBadRequest.Code
- } else if clientError, serverError := Mailer.Unsubscribe(unsubscribeCode); clientError != nil {
- slog.Info("got a client error when unsubscribing", slog.String("error", clientError.Error()))
- statusColor = "255, 0, 0"
- statusEmoji = "(͠≖~≖ ͡ )"
- statusText = l[lang].UnsubscribePage.InvalidCode
- status = fiber.ErrBadRequest.Code
- } else if serverError != nil {
- slog.Error("got a server error when unsubscribing", slog.String("error", serverError.Error()))
- statusColor = "255, 128, 0"
- statusEmoji = "( ˶°ㅁ°) !!"
- statusText = l[lang].UnsubscribePage.OnServerError
- status = fiber.ErrInternalServerError.Code
- } else {
- statusColor = "0, 255, 0"
- statusEmoji = "♡⸜(˶˃ ᵕ ˂˶)⸝♡"
- statusText = l[lang].UnsubscribePage.Success
- status = fiber.StatusOK
- }
-
- content, err := tm.Render("unsubscribe-page", fiber.Map{
- "Lang": lang,
- "L": l[lang],
- "StatusEmoji": statusEmoji,
- "StatusText": statusText,
- "StatusColor": template.HTML(statusColor),
- })
- if err != nil {
- slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/user/unsubscribe"), slog.String("error", err.Error()))
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
- }
-
- return c.Type("html").Status(status).Send(content)
- }
-}
diff --git a/internal/router/root.go b/internal/router/root.go
new file mode 100644
index 0000000..f4a0692
--- /dev/null
+++ b/internal/router/root.go
@@ -0,0 +1,27 @@
+package router
+
+import (
+ "log/slog"
+
+ "github.com/SayaAndy/saya-today-web/config"
+ "github.com/gofiber/fiber/v2"
+)
+
+func init() {
+ tm.Add("index", "views/index.html")
+}
+
+func Root(localeCfg []config.AvailableLanguageConfig) func(c *fiber.Ctx) error {
+ return func(c *fiber.Ctx) error {
+ content, err := tm.Render("index", fiber.Map{
+ "AvailableLanguages": localeCfg,
+ })
+ if err != nil {
+ slog.Warn("failed to generate page", slog.String("page", "/"), slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
+ }
+
+ return c.Type("html").Send(content)
+ }
+}
diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go
index bafdee5..eebc297 100644
--- a/internal/templatemanager/templatemanager.go
+++ b/internal/templatemanager/templatemanager.go
@@ -31,7 +31,6 @@ var templateFuncMap = template.FuncMap{
}
return items
},
- "replace": strings.ReplaceAll,
}
func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager, error) {
diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml
index c7f2df8..e93ab00 100644
--- a/locale/localization.en.yaml
+++ b/locale/localization.en.yaml
@@ -22,53 +22,6 @@ HomePage:
Hymn2: "We've been expecting you!"
Hymn3: 'You bring such joy in Sayasite!'
Hymn4: 'No matter where you roam, know our love is true!~'
-UnsubscribePage:
- Header: 'Cancelling the Subscription'
- UnsetCode: 'Your cancel code is not set. You just having a walk around the site?'
- InvalidCode: "Your cancel code could not be verified. Maybe, the code expired after a day. Or maybe, you've already unsubscribed."
- OnServerError: "We couldn't unsubscribe you, the problem is on our side! We will solve this problem as soon as possible."
- Success: 'Cancelled your subscription successfully! We do hope we will engage you again someday.'
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!'
- Intro: 'There are 2 ways for us to validate your profile:'
- GotoLink: 'Go by this link:'
- 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: 'A new post arrived at SAYA.TODAY!'
- Intro: 'A new post came!'
- CapturedOn: 'Captured on'
-UserProfile:
- Header: 'Personal Settings'
- EmailHeader: 'E-Mail'
- VerificationCodeHeader: 'Verification Code'
- TypeEmail: 'Please type your email'
- SendCodeButton: 'Send Code'
- VerifyButton: 'Verify'
- VerificationCodeSent: 'Verification code sent! Please check your e-mail. Your next verification attempt will be possible on {}.'
- DelayTilVerification: 'You have already requested the verification before! Please wait until {} for a new verification request.'
- FailedEmailRender: 'Failed to render the email status message, please ask administrator for a more detailed cause.'
- VerificationCodeSendingError: 'Failed to send a verification code, please ask administrator for a more detailed cause.'
- EmailAlreadyValidated: 'You already have this E-Mail validated.'
- EmailTaken: 'This E-Mail is already taken.'
- EmailEmpty: 'E-Mail is unset or empty.'
- VerificationSuccess: 'Success! You are now able to subscribe with your new e-mail.'
- VerificationFailed: 'Verification code is invalid, please ask administrator for a more detailed cause.'
- VerificationEmpty: 'Verification code is unset or empty.'
- SubscriptionHeader: 'Subscribe to...'
- TagsNone: 'None'
- TagsAll: 'All'
- TagsSpecific: 'Specific'
- TagDoesNotExist: 'The tag "{}" does not exist on the site.'
- TagAlreadyAdded: 'The tag "{}" is already in your subscribed list.'
- SaveButton: 'Save'
- SubscribeInvalidType: 'Invalid subscription type.'
- FailedToSubscribe: 'Failed to subscribe by new settings, please ask administrator for a more detailed cause.'
- SubscribedSuccessfully: 'Successfully subscribed you by new settings!'
- RefreshPage: 'Please refresh the profile page on the device, where you requested the code.'
diff --git a/locale/localization.go b/locale/localization.go
index c6467c4..7dbb554 100644
--- a/locale/localization.go
+++ b/locale/localization.go
@@ -8,14 +8,11 @@ import (
)
type LocaleConfig struct {
- TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"`
- BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"`
- GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"`
- HomePage HomePageConfig `yaml:"HomePage" json:"HomePage"`
- UnsubscribePage UnsubscribePageConfig `yaml:"UnsubscribePage" json:"UnsubscribePage"`
- Metadata MetadataConfig `yaml:"Metadata" json:"Metadata"`
- Mail MailConfig `yaml:"Mail" json:"Mail"`
- UserProfile UserProfileConfig `yaml:"UserProfile" json:"UserProfile"`
+ TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"`
+ BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"`
+ GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"`
+ HomePage HomePageConfig `yaml:"HomePage" json:"HomePage"`
+ Metadata MetadataConfig `yaml:"Metadata" json:"Metadata"`
}
type BlogSearchConfig struct {
@@ -47,70 +44,11 @@ type HomePageConfig struct {
Hymn4 string `yaml:"Hymn4" json:"Hymn4"`
}
-type UnsubscribePageConfig struct {
- Header string `yaml:"Header" json:"Header"`
- UnsetCode string `yaml:"UnsetCode" json:"UnsetCode"`
- InvalidCode string `yaml:"InvalidCode" json:"InvalidCode"`
- OnServerError string `yaml:"OnServerError" json:"OnServerError"`
- Success string `yaml:"Success" json:"Success"`
-}
-
type MetadataConfig struct {
Action string `yaml:"Action" json:"Action"`
Published string `yaml:"Published" json:"Published"`
}
-type MailConfig struct {
- UnsubscribeFooter string `yaml:"UnsubscribeFooter" json:"UnsubscribeFooter"`
- VerifyEmail VerifyEmailConfig `yaml:"VerifyEmail" json:"VerifyEmail"`
- NewPost NewPostConfig `yaml:"NewPost" json:"NewPost"`
-}
-
-type VerifyEmailConfig struct {
- Subject string `yaml:"Subject" json:"Subject"`
- Welcome string `yaml:"Welcome" json:"Welcome"`
- Intro string `yaml:"Intro" json:"Intro"`
- GotoLink string `yaml:"GotoLink" json:"GotoLink"`
- InputCode string `yaml:"InputCode" json:"InputCode"`
- IfRandom string `yaml:"IfRandom" json:"IfRandom"`
-}
-
-type NewPostConfig struct {
- Subject string `yaml:"Subject" json:"Subject"`
- CapturedOn string `yaml:"CapturedOn" json:"CapturedOn"`
- Intro string `yaml:"Intro" json:"Intro"`
-}
-
-type UserProfileConfig struct {
- Header string `yaml:"Header" json:"Header"`
- EmailHeader string `yaml:"EmailHeader" json:"EmailHeader"`
- VerificationCodeHeader string `yaml:"VerificationCodeHeader" json:"VerificationCodeHeader"`
- TypeEmail string `yaml:"TypeEmail" json:"TypeEmail"`
- SendCodeButton string `yaml:"SendCodeButton" json:"SendCodeButton"`
- VerifyButton string `yaml:"VerifyButton" json:"VerifyButton"`
- VerificationCodeSent string `yaml:"VerificationCodeSent" json:"VerificationCodeSent"`
- DelayTilVerification string `yaml:"DelayTilVerification" json:"DelayTilVerification"`
- FailedEmailRender string `yaml:"FailedEmailRender" json:"FailedEmailRender"`
- VerificationCodeSendingError string `yaml:"VerificationCodeSendingError" json:"VerificationCodeSendingError"`
- EmailAlreadyValidated string `yaml:"EmailAlreadyValidated" json:"EmailAlreadyValidated"`
- EmailTaken string `yaml:"EmailTaken" json:"EmailTaken"`
- EmailEmpty string `yaml:"EmailEmpty" json:"EmailEmpty"`
- VerificationSuccess string `yaml:"VerificationSuccess" json:"VerificationSuccess"`
- VerificationFailed string `yaml:"VerificationFailed" json:"VerificationFailed"`
- VerificationEmpty string `yaml:"VerificationEmpty" json:"VerificationEmpty"`
- SubscriptionHeader string `yaml:"SubscriptionHeader" json:"SubscriptionHeader"`
- TagsNone string `yaml:"TagsNone" json:"TagsNone"`
- TagsAll string `yaml:"TagsAll" json:"TagsAll"`
- TagsSpecific string `yaml:"TagsSpecific" json:"TagsSpecific"`
- TagDoesNotExist string `yaml:"TagDoesNotExist" json:"TagDoesNotExist"`
- TagAlreadyAdded string `yaml:"TagAlreadyAdded" json:"TagAlreadyAdded"`
- SaveButton string `yaml:"SaveButton" json:"SaveButton"`
- SubscribeInvalidType string `yaml:"SubscribeInvalidType" json:"SubscribeInvalidType"`
- FailedToSubscribe string `yaml:"FailedToSubscribe" json:"FailedToSubscribe"`
- SubscribedSuccessfully string `yaml:"SubscribedSuccessfully" json:"SubscribedSuccessfully"`
- RefreshPage string `yaml:"RefreshPage" json:"RefreshPage"`
-}
-
func LoadConfig(path string, config *LocaleConfig) error {
fileBytes, err := os.ReadFile(path)
if err != nil {
diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml
index 5df82da..a1a9a6c 100644
--- a/locale/localization.ru.yaml
+++ b/locale/localization.ru.yaml
@@ -22,53 +22,6 @@ HomePage:
Hymn2: 'Мы Вас столько ждали!'
Hymn3: 'Столько радости от Вас на Саясайте!'
Hymn4: 'Где бы Вы не были, знайте, -- наша любовь крепка!'
-UnsubscribePage:
- Header: 'Отписка'
- UnsetCode: 'Код для отписки не проставлен. Вы случайно не заблудились?'
- InvalidCode: 'Код для отписки не подтверждён. Может, истекло время в сутки для кода. А может, вы уже успели отписаться.'
- OnServerError: 'Мы не смогли вас отписать, проблема на нашей стороне! В скором времени займёмся проблемой.'
- Success: 'Вы отписались от рассылки! Надеюсь, мы ещё сможем вас заинтересовать.'
Metadata:
Published: 'Опубликовано'
Action: 'Время действия'
-Mail:
- UnsubscribeFooter: 'Если данное письмо пришло вам случайно, либо вы хотите отписаться, можете перейти по {}этой ссылке{/}.'
- VerifyEmail:
- Subject: 'Ссылка для верификации для подтверждения вашей почты SAYA.TODAY'
- Welcome: 'Приветствую!'
- Intro: 'Есть 2 способа, чтобы подтвердить свою электронную почту:'
- GotoLink: 'Перейти по этой ссылке:'
- InputCode: 'Либо ввести этот код на странице подписок:'
- IfRandom: 'Если ты не понимаешь, что это за спам, можешь проигнорировать это письмо, а ещё, если интересно, можешь посетить мой сайт :)'
- NewPost:
- Subject: 'Новый пост на SAYA.TODAY!'
- Intro: 'Вышел новый пост!'
- CapturedOn: 'Снималось'
-UserProfile:
- Header: 'Личные настройки'
- EmailHeader: 'E-Mail'
- VerificationCodeHeader: 'Код верификации'
- TypeEmail: 'Пожалуйста, введите свою почту'
- SendCodeButton: 'Отправить код'
- VerifyButton: 'Верифицировать'
- VerificationCodeSent: 'Выслали вам код верификации! Проверьте свою почту. Следующая попытка верификации будет возможна {}.'
- DelayTilVerification: 'Вы уже запрашивали недавно верификацию! Дождитесь {} для новой попытки.'
- FailedEmailRender: 'Неудачная попытка создать электронное письмо, более подробно спрашивайте у администратора.'
- VerificationCodeSendingError: 'Неудачная попытка прислать код для верификации, более подробно спрашивайте у администратора.'
- EmailAlreadyValidated: 'Эту почту вы уже подтверждали.'
- EmailTaken: 'Эта почта уже занята.'
- EmailEmpty: 'Почта не введена.'
- VerificationSuccess: 'Поздравляем! Теперь по этой почте можно подписаться на блог.'
- VerificationFailed: 'Код для верификации невалиден, при вопросах, пожалуйста, обращайтесь к администратору.'
- VerificationEmpty: 'Код для верификации не введён.'
- SubscriptionHeader: 'Подписаться на...'
- TagsNone: 'Ничего'
- TagsAll: 'Всё'
- TagsSpecific: 'Определённое'
- TagDoesNotExist: 'Тэга "{}" нет на сайте.'
- TagAlreadyAdded: 'Тэг "{}" уже у вас добавлен.'
- SaveButton: 'Сохранить'
- SubscribeInvalidType: 'Неизвестный тип подписки.'
- FailedToSubscribe: 'Неудачная попытка записать новую подписку, более подробно спрашивайте у администратора.'
- SubscribedSuccessfully: 'Вы успешно подписались по новым настройкам!'
- RefreshPage: 'Пожалуйста, обновите страницу с профилем на устройстве, откуда запрашивали код.'
diff --git a/main.go b/main.go
index 174854d..39fa0ab 100644
--- a/main.go
+++ b/main.go
@@ -11,10 +11,8 @@ 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"
"github.com/SayaAndy/saya-today-web/internal/router"
"github.com/SayaAndy/saya-today-web/internal/tailwind"
"github.com/SayaAndy/saya-today-web/locale"
@@ -146,31 +144,9 @@ func main() {
os.Exit(1)
}
- router.Mailer, err = mailer.NewMailer(db, cfg.Mail.ClientHost, cfg.Mail.MailHost,
- cfg.Mail.PublicName, cfg.Mail.MailAddress, cfg.Mail.Username, cfg.Mail.Password, []byte(cfg.Mail.Salt), localization)
- if err != nil {
- slog.Error("fail to initialize mailer", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- router.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(b2Client, cfg.AvailableLanguages, cfg.Mail.Trigger.OnNewPost, 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))
- app.Get("/:lang/user", router.Api_V1_GeneralPage(localization, availableLanguages))
- app.Get("/:lang/user/unsubscribe", router.Lang_User_Unsubscribe(localization, availableLanguages))
app.Get("/:lang/blog", router.Api_V1_GeneralPage(localization, availableLanguages))
app.Get("/:lang/blog/:title", router.Api_V1_GeneralPage(localization, availableLanguages))
@@ -183,10 +159,6 @@ func main() {
app.Get("/api/v1/general-page/footer", router.Api_V1_GeneralPage_Footer(localization, availableLanguages))
app.Get("/api/v1/general-page/top-embeds", router.Api_V1_GeneralPage_TopEmbeds(localization, availableLanguages))
app.Get("/api/v1/general-page/bottom-embeds", router.Api_V1_GeneralPage_BottomEmbeds(localization, availableLanguages, b2Client))
- app.Post("/api/v1/email/send-verification-code", router.Api_V1_Email_SendVerificationCode(localization))
- app.Post("/api/v1/email/verify", router.Api_V1_Email_Verify(localization))
- app.Get("/api/v1/email/is-in-verification", router.Api_V1_Email_IsInVerification(localization))
- app.Put("/api/v1/subs", router.Api_V1_Subs_Put(localization))
app.Static("/", "./static")
@@ -202,9 +174,6 @@ func main() {
<-sigChan
slog.Info("gracefully shutting down...")
- if err = router.BlogTrigger.Close(); err != nil {
- slog.Error("fail to shutdown blog trigger scheduler", slog.String("error", err.Error()))
- }
if err = app.Shutdown(); err != nil {
slog.Error("fail to shutdown fiber server", slog.String("error", err.Error()))
}
diff --git a/migrations/2_create_subscription_tables.down.sql b/migrations/2_create_subscription_tables.down.sql
deleted file mode 100644
index d9a369e..0000000
--- a/migrations/2_create_subscription_tables.down.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-DROP INDEX IF EXISTS user_email_table_email_uindex;
-DROP TABLE IF EXISTS user_email_table;
-DROP TABLE IF EXISTS subscription_user_to_tags_table;
-DROP TABLE IF EXISTS subscription_tag_to_users_table;
diff --git a/migrations/2_create_subscription_tables.up.sql b/migrations/2_create_subscription_tables.up.sql
deleted file mode 100644
index e6e905a..0000000
--- a/migrations/2_create_subscription_tables.up.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE IF NOT EXISTS user_email_table (
- user_id VARCHAR(32) NOT NULL PRIMARY KEY,
- email VARCHAR(64) NOT NULL,
- lang VARCHAR(2) NOT NULL
-) WITHOUT ROWID;
-
-CREATE UNIQUE INDEX user_email_table_email_uindex
-ON user_email_table(email);
-
-CREATE TABLE IF NOT EXISTS subscription_user_to_tags_table (
- user_id VARCHAR(32) NOT NULL PRIMARY KEY,
- tags TEXT NOT NULL
-) WITHOUT ROWID;
-
-CREATE TABLE IF NOT EXISTS subscription_tag_to_users_table (
- tag VARCHAR(32) NOT NULL PRIMARY KEY,
- user_ids TEXT NOT NULL
-) WITHOUT ROWID;
diff --git a/static/input.css b/static/input.css
index 739caa3..e153b41 100644
--- a/static/input.css
+++ b/static/input.css
@@ -9,7 +9,6 @@
html {
font-size: 12px;
- --crossed-out: url("data:image/svg+xml,<svg id='patternId' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='30' height='30' patternTransform='scale(1) rotate(65)'><rect x='0' y='0' width='100%' height='100%' fill='%232b2b3100' /><path d='M30 15L22.5 30 15 15 22.5 0zm-15 0L7.5 30 0 15 7.5 0z' stroke-width='0.6' stroke='%23ffffffff' fill='none' /></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)' /></svg>");
--noise-image: url(data:image/png;base64,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);
--split-left-background: unset;
--split-left-repeat: no-repeat;
@@ -146,10 +145,10 @@ html {
--leaflet-text-color: var(--main-hard-color);
--leaflet-link-color: var(--main-medium-color);
--leaflet-link-hover-color: var(--main-soft-color);
- --interlocked-hexagons-background: url("data:image/svg+xml,<svg width='160' height='160' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='40' height='80' patternTransform='scale(2) rotate(0)'><rect x='0' y='0' width='100%' height='100%' fill='%23e3e6ffff'/><path d='M-10 7.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%238c4351ff' fill='none'/><path d='M-10 27.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23385f0dff' fill='none'/><path d='M-10 47.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23006c86ff' fill='none'/><path d='M-10 67.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%235a3e8eff' fill='none'/></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)'/></svg>");
+ --interlocked-hexagons-background: url("data:image/svg+xml,<svg id='patternId' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='40' height='80' patternTransform='scale(2) rotate(0)'><rect x='0' y='0' width='100%' height='100%' fill='%23e3e6ffff'/><path d='M-10 7.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%238c4351ff' fill='none'/><path d='M-10 27.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23385f0dff' fill='none'/><path d='M-10 47.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23006c86ff' fill='none'/><path d='M-10 67.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%235a3e8eff' fill='none'/></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)'/></svg>");
--interlocked-hexagons-background-repeat: repeat;
--interlocked-hexagons-background-size: auto;
- --squares-and-triangles-background: url("data:image/svg+xml,<svg width='160' height='160' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='40' height='80' patternTransform='scale(2) rotate(0)'><rect x='0' y='0' width='100%' height='100%' fill='%23e3e6ffff'/><path d='M-10 7.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%238c4351ff' fill='none'/><path d='M-10 27.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23385f0dff' fill='none'/><path d='M-10 47.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23006c86ff' fill='none'/><path d='M-10 67.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%235a3e8eff' fill='none'/></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)'/></svg>");
+ --squares-and-triangles-background: url("data:image/svg+xml,<svg id='patternId' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='40' height='80' patternTransform='scale(2) rotate(0)'><rect x='0' y='0' width='100%' height='100%' fill='%23e3e6ffff'/><path d='M-10 7.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%238c4351ff' fill='none'/><path d='M-10 27.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23385f0dff' fill='none'/><path d='M-10 47.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23006c86ff' fill='none'/><path d='M-10 67.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%235a3e8eff' fill='none'/></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)'/></svg>");
--squares-and-triangles-background-repeat: repeat;
--squares-and-triangles-background-width-coef: 5;
--squares-and-triangles-background-height-coef: 5;
@@ -180,27 +179,6 @@ a:hover, .linklike:hover {
text-decoration: underline;
}
-.crossable.disabled::before,
-.crossable.htmx-request::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: var(--main-hard-color);
- background-size: 0.5rem 0.5rem;
- mask-image: var(--crossed-out);
- opacity: 1;
- transition: opacity 0.3s;
- z-index: 25;
-}
-
-form.crossable.disabled input,
-form.crossable.htmx-request input {
- filter: grayscale(1);
-}
-
.bg-interlocked-hexagons {
background-image: var(--interlocked-hexagons-background);
background-repeat: var(--interlocked-hexagons-background-repeat);
@@ -467,18 +445,12 @@ form.crossable.htmx-request input {
border-color: transparent transparent var(--background-dark-color) transparent;
}
-.grid-item {
+.grid-item, .grid-sizer {
position: relative;
display: inline-block;
width: 10rem;
}
-.grid-sizer {
- position: relative;
- display: inline-block;
- width: 10.1rem;
-}
-
.grid-item-index {
visibility: visible;
opacity: 1;
@@ -503,42 +475,30 @@ form.crossable.htmx-request input {
}
@container main (width >= 40rem) {
- .grid-item {
+ .grid-item, .grid-sizer {
width: 12rem;
}
- .grid-sizer {
- width: 12.1rem;
- }
-
.grid-item-2x {
width: 24rem;
}
}
@container main (width >= 76rem) {
- .grid-item {
+ .grid-item, .grid-sizer {
width: 18rem;
}
- .grid-sizer {
- width: 18.1rem;
- }
-
.grid-item-2x {
width: 36rem;
}
}
@container main (width >= 128rem) {
- .grid-item {
+ .grid-item, .grid-sizer {
width: 24rem;
}
- .grid-sizer {
- width: 24.1rem;
- }
-
.grid-item-2x {
width: 48rem;
}
diff --git a/views/layouts/general-mail.html b/views/layouts/general-mail.html
deleted file mode 100644
index 8f42c7a..0000000
--- a/views/layouts/general-mail.html
+++ /dev/null
@@ -1,105 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
- <meta charset="UTF-8">
- <head>
- <style>
-@font-face {
- font-family: 'Philosopher';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: url(https://fonts.gstatic.com/s/philosopher/v21/vEFV2_5QCwIS4_Dhez5jcWBuT0020Nqf.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-@font-face {
- font-family: 'Philosopher';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: url(https://fonts.gstatic.com/s/philosopher/v21/vEFV2_5QCwIS4_Dhez5jcWBqT0020NqfZ7c.woff2) format('woff2');
- unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
-}
-
-html {
- font-family: 'Philosopher', sans-serif;
- --noise-image: url(data:image/png;base64,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);
-}
-
-body {
- background-color: #e3e6ff;
-}
-
-a {
- text-decoration: underline;
- text-shadow: none !important;
-}
-
-.header {
- width: 100%;
- padding: 8px 8px 8px 32px;
- font-size: 48px;
- font-family: 'Philosopher';
- color: white;
- text-shadow: -2px 0 #343b58, 0 2px #343b58, 2px 0 #343b58, 0 -2px #343b58;
- background: url("data:image/svg+xml,<svg width='160' height='160' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='40' height='80' patternTransform='scale(2) rotate(0)'><rect x='0' y='0' width='100%' height='100%' fill='%23e3e6ffff'/><path d='M-10 7.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%238c4351ff' fill='none'/><path d='M-10 27.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23385f0dff' fill='none'/><path d='M-10 47.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23006c86ff' fill='none'/><path d='M-10 67.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%235a3e8eff' fill='none'/></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)'/></svg>");
- background-color: #e3e6ff;
- background-size: contain;
- background-repeat: repeat;
-}
-
-.content {
- 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;
- background-color: #c3caff;
- 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>
-
- <body>
- <div id="header" class="header">
- saya.today
- </div>
-
- <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 a7281e8..920640e 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-dvh flex flex-row">
+<body data-theme="ram" class="font-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-[100dvh] flex flex-row">
<script>
let map = null;
@@ -156,7 +156,7 @@
switchThemeDecor(theme);
}
- const savedTheme = localStorage.getItem('theme') || 'lilac';
+ const savedTheme = localStorage.getItem('theme') || 'ram';
switchTheme(savedTheme);
</script>
@@ -172,23 +172,20 @@
<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-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 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 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>
- </a>
- <a href="/{{ .Lang }}" id="sidebar-home-button" class="w-[5rem] h-[5rem] hidden">
+ <a href="/{{ .Lang }}" id="sidebar-home-button" class="w-[5rem] h-[5rem]">
<i onclick="return changeUrl('/{{ .Lang }}');" class="fas fa-home flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
</a>
- <a href="/{{ .Lang }}/blog" id="sidebar-blog-button" class="w-[5rem] h-[5rem] hidden">
+ <a href="/{{ .Lang }}/blog" id="sidebar-blog-button" class="w-[5rem] h-[5rem]">
<i onclick="return changeUrl('/{{ .Lang }}/blog');" class="fas fa-search flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
</a>
- <a href="/{{ .Lang }}/map" id="sidebar-map-button" class="w-[5rem] h-[5rem] hidden">
+ <a href="/{{ .Lang }}/map" id="sidebar-map-button" class="w-[5rem] h-[5rem]">
<i class="fas fa-map flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
</a>
<i onclick="toggleThemeWheel();" id="sidebar-theme-button" class="fas fa-swatchbook flex w-[5rem] h-[5rem] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 rounded-lg"></i>
- <a href="/{{ .Lang }}/user" id="sidebar-user-button" class="w-[5rem] h-[5rem] hidden">
- <i onclick="return changeUrl('/{{ .Lang }}/user');" class="fas fa-user flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ <a href="./" id="sidebar-back-button" class="w-[5rem] h-[5rem]">
+ <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>
</a>
</div>
<div class="theme-wheel" id="theme-wheel">
@@ -204,7 +201,6 @@
sidebarMapButton = document.getElementById("sidebar-map-button");
sidebarBlogButton = document.getElementById("sidebar-blog-button");
sidebarBackButton = document.getElementById("sidebar-back-button");
- sidebarUserButton = document.getElementById("sidebar-user-button");
function updateSidebar() {
if (location.pathname == "" || location.pathname == "/") {
@@ -212,21 +208,17 @@
sidebarMapButton.classList.add('hidden');
sidebarBlogButton.classList.add('hidden');
sidebarBackButton.classList.add('hidden');
- sidebarUserButton.classList.add('hidden');
} else {
sidebarHomeButton.classList.remove('hidden');
sidebarMapButton.classList.remove('hidden');
sidebarBlogButton.classList.remove('hidden');
sidebarBackButton.classList.remove('hidden');
- sidebarUserButton.classList.remove('hidden');
lang = location.pathname.split("/")[1];
sidebarHomeButton.setAttribute('href', `/${lang}`);
sidebarHomeButton.firstElementChild.setAttribute('onclick', `return changeUrl('/${lang}', false);`);
sidebarMapButton.setAttribute('href', `/${lang}/map`);
sidebarBlogButton.setAttribute('href', `/${lang}/blog`);
sidebarBlogButton.firstElementChild.setAttribute('onclick', `return changeUrl('/${lang}/blog', false);`);
- sidebarUserButton.setAttribute('href', `/${lang}/user`);
- sidebarUserButton.firstElementChild.setAttribute('onclick', `return changeUrl('/${lang}/user', false);`);
}
}
window.addEventListener("popstate", updateSidebar);
@@ -234,7 +226,7 @@
</script>
</div>
- <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 @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 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
deleted file mode 100644
index f1338f7..0000000
--- a/views/messages/new-post.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{{ define "body" }}
-<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 style="color: #273de1 !important;" 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>{{ .UnsubscribeFooter }}</p>
-{{ end }}
diff --git a/views/messages/verify-email.html b/views/messages/verify-email.html
deleted file mode 100644
index dd6ef33..0000000
--- a/views/messages/verify-email.html
+++ /dev/null
@@ -1,10 +0,0 @@
-{{ define "body" }}
-<p>{{ .L.Mail.VerifyEmail.Welcome }}</p>
-<p>{{ .L.Mail.VerifyEmail.Intro }}</p>
-<ol>
- <li>{{ .L.Mail.VerifyEmail.GotoLink }} <a href="https://{{ .ClientHost }}/{{ .Lang }}/user?email_code={{ .VerificationCode }}" class="outlier">&gt;&gt;&gt;</a></li>
- <li>{{ .L.Mail.VerifyEmail.InputCode }} <span class="outlier">{{ .VerificationCode }}</span><span></li>
-</ol>
-<p>{{ .L.Mail.VerifyEmail.IfRandom }}</p>
-<a href="https://{{ .ClientHost }}" class="outlier">{{ .ClientHost }}</a>
-{{ end }} \ No newline at end of file
diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html
index d209205..8e91798 100644
--- a/views/pages/blog-catalogue.html
+++ b/views/pages/blog-catalogue.html
@@ -1,10 +1,10 @@
{{ define "body" }}
-<div class="flex flex-row ar-lt-0.8:flex-col text-base h-full">
+<div class="flex flex-row ar-lt-0.8:flex-col text-base h-[100%]">
<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-[10dvh] ar-gt-0.8:max-w-[20dvh] ar-gt-0.8:w-fit ar-lt-0.8:w-full ar-lt-0.8:max-h-[30%] flex shrink-0 flex-col ar-gt-0.8:mr-6 bg-background-light inset-shadow-[0_0_0.4rem_black]">
+ class="tags-list ar-gt-0.8:min-w-[10dvh] ar-gt-0.8:max-w-[20dvh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] ar-lt-0.8:max-h-[30%] flex shrink-0 flex-col ar-gt-0.8:mr-6 bg-background-light inset-shadow-[0_0_0.4rem_black]">
<div class="flex flex-col overflow-y-auto">
<fieldset>
- <legend class="font-bold w-full text-center">{{ .L.BlogSearch.TagsHeader }}</legend>
+ <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.TagsHeader }}</legend>
<div class="flex flex-col ar-lt-0.8:grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-5 grid-flow-row-dense">
<div class="m-1">
<label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();"> {{ .L.BlogSearch.ChooseAllTags }}</label>
@@ -17,7 +17,7 @@
</div>
</fieldset>
<fieldset class="mt-1">
- <legend class="font-bold w-full text-center">{{ .L.BlogSearch.OrderByHeader }}</legend>
+ <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-1">
<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>
diff --git a/views/pages/global-map.html b/views/pages/global-map.html
index 306f442..b9d2472 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-dvh">
+<body data-theme="ram" class="font-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-[100dvh]">
<script>
function switchTheme(theme) {
@@ -30,7 +30,7 @@
document.body.setAttribute('data-theme', theme);
}
- const savedTheme = localStorage.getItem('theme') || 'lilac';
+ const savedTheme = localStorage.getItem('theme') || 'ram';
switchTheme(savedTheme);
</script>
@@ -44,9 +44,6 @@
<div id="sidebar" class="bg-sidebar block fixed shadow-[0_0_0.4rem_black] z-1001 w-[5rem] h-auto">
<div class="text-[3rem] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1dvw] flex flex-col gap-0 relative z-1001">
- <a href="./" class="w-[5rem] h-[5rem]">
- <i 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>
- </a>
<a href="/{{ .Lang }}" class="w-[5rem] h-[5rem]">
<i class="fas fa-home flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
</a>
@@ -57,8 +54,8 @@
<i class="fas fa-map flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
</a>
<i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook flex w-[5rem] h-[5rem] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 rounded-lg"></i>
- <a href="/{{ .Lang }}/user" id="sidebar-user-button" class="w-[5rem] h-[5rem]">
- <i onclick="return changeUrl('/{{ .Lang }}/user');" class="fas fa-user flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ <a href="./" class="w-[5rem] h-[5rem]">
+ <i 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>
</a>
</div>
<div class="theme-wheel" id="theme-wheel">
diff --git a/views/pages/unsubscribe-page.html b/views/pages/unsubscribe-page.html
deleted file mode 100644
index 3876818..0000000
--- a/views/pages/unsubscribe-page.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>{{ .L.UnsubscribePage.Header }} // SAYA TODAY</title>
-<style>
- html {
- --noise-image: url(data:image/png;base64,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);
- }
-</style>
-</head>
-
-<body style="width: 100dvw; height: 100dvh; display: flex; flex-direction: column; position: fixed">
-
- <div style='flex: 3 0 0; background-image: var(--noise-image); background-color: rgba({{ .StatusColor }}, 0.1)'></div>
-
- <div style="flex: 1 0 0; background-image: var(--noise-image); align-content: center;">
- <p style="position: relative; width: 100%; margin-inline: auto; text-align: center; font-size: 2rem;">{{ .StatusEmoji }}</p>
- </div>
-
- <div style="flex: 1 0 0; background-image: var(--noise-image); align-content: center;">
- <p style="position: relative; width: 100%; margin-inline: auto; text-align: center; font-size: 1.5rem;">{{ .StatusText }}</p>
- </div>
-
- <div style='flex: 3 0 0; background-image: var(--noise-image); background-color: rgba({{ .StatusColor }}, 0.2)'></div>
-
-</body>
-
-</html>
diff --git a/views/pages/user-page.html b/views/pages/user-page.html
deleted file mode 100644
index 7d39825..0000000
--- a/views/pages/user-page.html
+++ /dev/null
@@ -1,205 +0,0 @@
-{{ define "body" }}
-<div class="bg-background-light flex flex-col inset-shadow-[0_0_0.4rem_black] px-8 py-4">
- <form id="email-form" class="w-full mb-2 flex flex-col relative crossable"
- hx-get="/api/v1/email/is-in-verification" hx-target="#email-message" hx-swap="outerHTML" hx-trigger="load" hx-indicator="this">
- <div class="flex flex-row w-full mb-4 z-21">
- <div class="flex flex-col w-full sm:flex-3/5 grow-0">
- <label class="block text-main-medium font-bold mb-1 z-22">
- {{ .L.UserProfile.EmailHeader }}
- </label>
- <input class="bg-(--background-medium-color) appearance-none border-(--background-medium-color) rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-(--background-light-color) focus:text-main-hard z-22"
- id="email-input" name="email" type="text" value="{{ .Email }}" placeholder="{{ .L.UserProfile.TypeEmail }}" maxlength="64"
- data-validated-email="{{ .Email }}" onchange="checkIfValidated(this);">
- </div>
- </div>
- <button class="w-max mx-auto bg-(--main-hard-color) hover:bg-(--main-medium-color) active:bg-(--main-soft-color) active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-(--background-light-color) font-bold py-2 px-4 rounded z-22"
- hx-post="/api/v1/email/send-verification-code" hx-target="#email-message" hx-swap="outerHTML" hx-indicator="#email-form">
- {{ .L.UserProfile.SendCodeButton }}
- </button>
- </form>
- <div id="email-message" class="bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div>
-
- <hr class="my-2 border-t-4 border-dotted border-main-hard">
-
- <form id="email-verification-form" class="w-full mb-2 flex flex-col relative hidden crossable"
- {{ if .EmailCode }}hx-post="/api/v1/email/verify" hx-target="#verification-message" hx-swap="outerHTML" hx-trigger="load" hx-indicator="this"{{ end }}>
- <div class="flex flex-row w-full mb-4 z-21">
- <div class="flex flex-col w-full sm:flex-2/5 grow-0">
- <label class="block text-main-medium font-bold ml-1 z-22">
- {{ .L.UserProfile.VerificationCodeHeader }}
- </label>
- <input class="bg-(--background-medium-color) appearance-none border-(--background-medium-color) rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-(--background-light-color) focus:text-main-hard z-22"
- name="email_code" type="text" value="{{ .EmailCode }}" placeholder="0123456789ABCDEF" maxlength="16" autocapitalize="characters">
- </div>
- </div>
- <button class="w-max mx-auto bg-(--main-hard-color) hover:bg-(--main-medium-color) active:bg-(--main-soft-color) active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-(--background-light-color) font-bold py-2 px-4 rounded z-22"
- hx-post="/api/v1/email/verify" hx-target="#verification-message" hx-swap="outerHTML" hx-indicator="#email-verification-form">
- {{ .L.UserProfile.VerifyButton }}
- </button>
- </form>
- <div id="verification-message" class="bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div>
-</div>
-
-<hr class="my-4 border-t-4 border-dotted border-main-hard">
-
-<div class="bg-background-light flex flex-col inset-shadow-[0_0_0.4rem_black] px-8 py-4">
- <form id="subs-form" class="w-full mb-2 flex flex-col relative crossable">
- <div class="flex flex-col w-full mb-4 z-21">
- <label class="block text-main-medium font-bold mb-1 z-22">
- {{ .L.UserProfile.SubscriptionHeader }}
- </label>
- <div class="flex flex-col sm:flex-row w-full">
- <div class="m-1 w-1/3">
- <label><input class="mr-1" type="radio" id="tagsNone" name="tags" value="none" {{ if eq .TagsPicked "none" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsNone }}</label>
- </div>
- <div class="m-1 w-1/3">
- <label><input class="mr-1" type="radio" id="tagsAll" name="tags" value="all" {{ if eq .TagsPicked "all" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsAll }}</label>
- </div>
- <div class="m-1 w-1/3">
- <label><input class="mr-1" type="radio" id="tagsSpecific" name="tags" value="specific" {{ if eq .TagsPicked "specific" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsSpecific }}</label>
- </div>
- </div>
- <div id="pick-tags-form" class="flex flex-col w-full relative crossable">
- <input type="text" class="bg-(--background-medium-color) appearance-none border-(--background-medium-color) rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-(--background-light-color) focus:text-main-hard z-22"
- list="existing-tags" onchange="addTag(this.value);" onkeydown="if (event.keyCode === 13) {addTag(this.value); this.value=''; return false;}">
- <datalist id="existing-tags">
- {{- range .ExistingTags }}
- <option value="{{ .Name }}">
- {{- end }}
- </datalist>
- <div id="tags-picked-list" class="bg-background-dark flex flex-wrap inset-shadow-[0_0_0.4rem_black] p-2 m-4 w-full">
- {{- range .TagsPickedList }}
- <div id="tagPicked{{ . }}" class="bg-main-hard text-background-light m-2 flex flex-row">
- <p class="py-1 px-2">{{ . }}</p>
- <p class="py-1 px-2 bg-main-soft cursor-pointer" onclick="htmx.remove(htmx.find('#tagPicked{{ . }}'));">✘</p>
- <input type="hidden" name="tags_picked" value="{{ . }}">
- </div>
- {{- end }}
- </div>
- </div>
- <button class="w-max mx-auto bg-(--main-hard-color) hover:bg-(--main-medium-color) active:bg-(--main-soft-color) active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-(--background-light-color) font-bold py-2 px-4 rounded z-22"
- hx-put="/api/v1/subs" hx-target="#subs-message" hx-swap="outerHTML" hx-indicator="#subs-form">
- {{ .L.UserProfile.SaveButton }}
- </button>
- </div>
- </form>
- <div id="subs-message" class="bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div>
-</div>
-
-<script>
-htmx.on("htmx:beforeSwap", (e) => {
- if (e.detail.xhr.status === 422) {
- e.detail.shouldSwap = true;
- e.detail.isError = false;
- }
-});
-
-var formAllowTimeout;
-var verificationHideTimeout;
-
-var url = new URL(window.location.href);
-url.searchParams.delete('email_code');
-window.history.pushState({}, '', url.toString());
-
-htmx.on("htmx:afterSwap", (e) => {
- emailMessage = htmx.find("#email-message");
- verificationMessage = htmx.find("#verification-message");
- emailForm = htmx.find("#email-form");
- emailInput = htmx.find("#email-input");
- emailVerificationForm = htmx.find("#email-verification-form");
- subsForm = htmx.find("#subs-form");
- tryAgainTimeDiff = parseInt(emailMessage.dataset.strikedEndTime || "0") - new Date().getTime();
- verificationWaitingTimeDiff = parseInt(emailMessage.dataset.codeExpiryTime || "0") - new Date().getTime();
-
- if (tryAgainTimeDiff > 0) {
- emailForm.classList.add("disabled");
- formAllowTimeout = setTimeout(() => {
- emailForm.classList.remove("disabled");
- emailMessage.classList.add("hidden");
- }, tryAgainTimeDiff);
- } else {
- emailForm.classList.remove("disabled");
- }
-
- if (verificationWaitingTimeDiff > 0) {
- verificationHideTimeout = setTimeout(() => emailVerificationForm.classList.add("hidden"), verificationWaitingTimeDiff);
- emailVerificationForm.classList.remove("hidden");
- } else {
- emailVerificationForm.classList.add("hidden");
- }
-
- if ((verificationMessage.dataset.hideVerificationPanel || "false") == "true") {
- emailVerificationForm.classList.add("hidden");
- }
-
- if (emailInput.dataset.validatedEmail != "") {
- subsForm.classList.remove("disabled");
- } else {
- subsForm.classList.add("disabled");
- }
-});
-
-function checkIfValidated(elem) {
- if ((elem.dataset.validatedEmail != "") && (elem.value == elem.dataset.validatedEmail)) {
- elem.classList.add("!bg-green-300/20");
- } else {
- elem.classList.remove("!bg-green-300/20");
- }
-}
-
-checkIfValidated(htmx.find("#email-input"));
-
-function toggleTagsPick(elem) {
- const pickTagsForm = htmx.find("#pick-tags-form");
- if (elem.value === "specific" && elem.checked) {
- pickTagsForm.classList.remove("disabled");
- } else {
- pickTagsForm.classList.add("disabled");
- }
-}
-
-toggleTagsPick(htmx.find("#tagsSpecific"));
-
-function addTag(name) {
- name = name.trim();
- const subsMessage = htmx.find("#subs-message");
- const existingTags = htmx.findAll(htmx.find("#existing-tags"), "option");
- found = [...existingTags].find(opt => opt.value===name);
- if (!found) {
- subsMessage.classList.remove("hidden");
- subsMessage.innerHTML = "{{ .L.UserProfile.TagDoesNotExist }}".replace("{}", name);
- return;
- }
-
- const tagsPickedList = htmx.find("#tags-picked-list");
- const subscribedTags = htmx.findAll(tagsPickedList, "div input");
- found = [...subscribedTags].find(opt => opt.value===name);
- if (found) {
- subsMessage.classList.remove("hidden");
- subsMessage.innerHTML = "{{ .L.UserProfile.TagAlreadyAdded }}".replace("{}", name);
- return;
- }
-
- tagsPickedList.innerHTML += `
- <div id="tagPicked${name}" class="bg-main-hard text-background-light m-2 flex flex-row">
- <p class="py-1 px-2">${name}</p>
- <p class="py-1 px-2 bg-main-soft cursor-pointer" onclick="htmx.remove(htmx.find('#tagPicked${name}'));">✘</p>
- <input type="hidden" name="tags_picked" value="${name}">
- </div>
- `;
-}
-
-htmx.on("htmx:configRequest", (e) => {
- const params = e.detail.parameters;
-
- if (Array.isArray(params.tags_picked)) {
- tags_join = params.tags_picked[0] || "";
- for (i = 1; i < params.tags_picked.length; i++) {
- tags_join += ",";
- tags_join += params.tags_picked[i];
- }
- params.tags_picked = tags_join;
- }
-})
-</script>
-{{ end }} \ No newline at end of file
diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html
index b50f419..9bc5a30 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-full">
+ <div class="flex flex-row gap-2 max-w-[100%]">
<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>
diff --git a/views/partials/personal-page-status.html b/views/partials/personal-page-status.html
deleted file mode 100644
index a4e47c2..0000000
--- a/views/partials/personal-page-status.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<div id="{{ .StatusId }}" class='z-26 w-full inset-shadow-[0_0_0.4rem_black]
- {{if eq .Status "OK"}}bg-green-700 text-green-100
- {{else if eq .Status "Failed"}}bg-red-700 text-red-100
- {{else}}bg-background-dark text-main-soft{{end}}
- {{if eq .Message ""}}hidden{{end}}
- py-2 px-4 text-center'
- {{- range $_, $val := .DataAttributes }}
- {{ $val }}
- {{- end }}
- >
- {{ .Message }}
-</div> \ No newline at end of file