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.go86
-rw-r--r--config/config.local.yaml56
-rw-r--r--config/config.prod.yaml60
-rw-r--r--config/config.stage.yaml57
-rw-r--r--deploy/inventory.yml2
-rw-r--r--deploy/playbook.yml28
-rw-r--r--go.mod59
-rw-r--r--go.sum119
-rw-r--r--internal/blog/index.go32
-rw-r--r--internal/blog/s3.go173
-rw-r--r--internal/router/handlers/root.go3
-rw-r--r--internal/router/router.go36
-rw-r--r--locale/localization.en.yaml120
-rw-r--r--locale/localization.ru.yaml132
-rw-r--r--main.go2
-rw-r--r--static/favicon-dark.svg16
-rw-r--r--static/favicon-light.svg16
-rw-r--r--static/favicon.icobin5694 -> 0 bytes
-rw-r--r--static/input.css125
-rw-r--r--views/layouts/general-mail.html196
-rw-r--r--views/layouts/general-page.html1026
-rw-r--r--views/pages/global-map.html616
-rw-r--r--views/pages/robots.txt4
-rw-r--r--views/pages/unsubscribe-page.html99
-rw-r--r--views/partials/general-page-footer.html26
-rw-r--r--views/partials/general-page-header.html10
28 files changed, 1992 insertions, 1111 deletions
diff --git a/.github/workflows/build-and-deploy-prod.yml b/.github/workflows/build-and-deploy-prod.yml
index d89cc76..97afdf7 100644
--- a/.github/workflows/build-and-deploy-prod.yml
+++ b/.github/workflows/build-and-deploy-prod.yml
@@ -80,4 +80,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 }}" -e saya_today_google_site_verification="${{ secrets.GOOGLE_SITE_VERIFICATION }}"
+ 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_s3_access_key_id="${{ secrets.S3_ACCESS_KEY_ID }}" -e saya_today_web_s3_secret_access_key="${{ secrets.S3_SECRET_ACCESS_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 }}" -e saya_today_google_site_verification="${{ secrets.GOOGLE_SITE_VERIFICATION }}" -e saya_today_yandex_verification="${{ secrets.YANDEX_VERIFICATION }}"
diff --git a/.github/workflows/build-and-deploy-stage.yml b/.github/workflows/build-and-deploy-stage.yml
index e699856..ffee091 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_s3_access_key_id="${{ secrets.S3_ACCESS_KEY_ID }}" -e saya_today_web_s3_secret_access_key="${{ secrets.S3_SECRET_ACCESS_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 }}"
diff --git a/config/config.go b/config/config.go
index 536a333..0b1570a 100644
--- a/config/config.go
+++ b/config/config.go
@@ -12,6 +12,7 @@ import (
type Config struct {
LogLevel slog.Level `json:"LogLevel" yaml:"logLevel" validate:"required"`
+ Endpoint EndpointConfig `json:"Endpoint" yaml:"endpoint" validate:"required"`
BlogPages BlogPagesConfig `json:"BlogPages" yaml:"blogPages" validate:"required"`
FactGiver FactGiverConfig `json:"FactGiver" yaml:"factGiver" validate:"required"`
LocalePath string `json:"LocalePath" yaml:"localePath" validate:"required,filepath"`
@@ -25,12 +26,90 @@ type Config struct {
AllowOrigins []string `json:"AllowOrigins" yaml:"allowOrigins"`
}
+type EndpointConfig struct {
+ Type string `json:"Type" yaml:"type" validate:"required,oneof=http unix"`
+ Config any `json:"Config" yaml:"config" validate:"required"`
+}
+
+func (ec *EndpointConfig) UnmarshalJSON(data []byte) error {
+ var tmp struct {
+ Type string `json:"Type"`
+ Config json.RawMessage `json:"Config"`
+ }
+
+ if err := json.Unmarshal(data, &tmp); err != nil {
+ return err
+ }
+
+ ec.Type = tmp.Type
+
+ switch tmp.Type {
+ case "http":
+ var httpConfig HttpConfig
+ if err := json.Unmarshal(tmp.Config, &httpConfig); err != nil {
+ return fmt.Errorf("unmarshal HttpConfig: %w", err)
+ }
+ ec.Config = &httpConfig
+ case "unix":
+ var unixConfig UnixConfig
+ if err := json.Unmarshal(tmp.Config, &unixConfig); err != nil {
+ return fmt.Errorf("unmarshal UnixConfig: %w", err)
+ }
+ ec.Config = &unixConfig
+ default:
+ return fmt.Errorf("unsupported storage type: %s", tmp.Type)
+ }
+
+ return nil
+}
+
+func (ec *EndpointConfig) UnmarshalYAML(value *yaml.Node) error {
+ var tmp struct {
+ Type string `yaml:"type"`
+ Config yaml.Node `yaml:"config"`
+ }
+
+ if err := value.Decode(&tmp); err != nil {
+ return err
+ }
+
+ ec.Type = tmp.Type
+
+ switch tmp.Type {
+ case "http":
+ var httpConfig HttpConfig
+ if err := tmp.Config.Decode(&httpConfig); err != nil {
+ return fmt.Errorf("unmarshal HttpConfig: %w", err)
+ }
+ ec.Config = &httpConfig
+ case "unix":
+ var unixConfig UnixConfig
+ if err := tmp.Config.Decode(&unixConfig); err != nil {
+ return fmt.Errorf("unmarshal UnixConfig: %w", err)
+ }
+ ec.Config = &unixConfig
+ default:
+ return fmt.Errorf("unsupported storage type: %s", tmp.Type)
+ }
+
+ return nil
+}
+
+type HttpConfig struct {
+ ListenOn string `json:"ListenOn" yaml:"listenOn" validate:"required"`
+}
+
+type UnixConfig struct {
+ Path string `json:"Path" yaml:"path" validate:"required"`
+ Chmod string `json:"Chmod" yaml:"chmod" validate:"oneof=0600 0660 0666"`
+}
+
type BlogPagesConfig struct {
Storage StorageConfig `json:"Storage" yaml:"storage" validate:"required"`
}
type StorageConfig struct {
- Type string `json:"type" yaml:"type,oneof=b2 s3"`
+ Type string `json:"Type" yaml:"type" validate:"required,oneof=b2 s3"`
Config any `json:"Config" yaml:"config" validate:"required"`
}
@@ -159,6 +238,7 @@ type TriggerConfig struct {
type MetaConfig struct {
GoogleSiteVerification string `json:"GoogleSiteVerification" yaml:"googleSiteVerification"`
+ YandexVerification string `json:"YandexVerification" yaml:"yandexVerification"`
}
type PhotoStorageConfig struct {
@@ -202,6 +282,10 @@ func InitConfig(path string) (*Config, error) {
return nil, err
}
+ if config.Endpoint.Type == "unix" && config.Endpoint.Config.(*UnixConfig).Chmod == "" {
+ config.Endpoint.Config.(*UnixConfig).Chmod = "0660"
+ }
+
validate := validator.New(validator.WithRequiredStructEnabled())
if err := validate.Struct(config); err != nil {
return nil, err
diff --git a/config/config.local.yaml b/config/config.local.yaml
index fb79e97..d3e2e33 100644
--- a/config/config.local.yaml
+++ b/config/config.local.yaml
@@ -1,23 +1,31 @@
logLevel: debug
+endpoint:
+ type: http
+ config:
+ listenOn: ":3000"
blogPages:
storage:
- type: b2
+ type: s3
config:
- bucketName: sayana-pages
- region: eu-central-003
- prefix: 'stage-'
- keyID: '${B2_KEY_ID}'
- applicationKey: '${B2_APPLICATION_KEY}'
+ bucketName: sayauz-pages
+ region: kz1
+ prefix: "stage-"
+ endpoint: "https://storage.yandexcloud.kz"
+ usePathStyle: true
+ accessKeyID: "${S3_ACCESS_KEY_ID}"
+ secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
factGiver:
storage:
- type: b2
+ type: s3
config:
- bucketName: sayana-pages
- region: eu-central-003
- prefix: ''
- keyID: '${B2_KEY_ID}'
- applicationKey: '${B2_APPLICATION_KEY}'
- factsFileName: 'facts-*.txt'
+ bucketName: sayauz-pages
+ region: kz1
+ prefix: ""
+ endpoint: "https://storage.yandexcloud.kz"
+ usePathStyle: true
+ accessKeyID: "${S3_ACCESS_KEY_ID}"
+ secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
+ factsFileName: "facts-*.txt"
localePath: ./locale/
availableLanguages:
- name: ru
@@ -33,19 +41,19 @@ auth:
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'
- salt: '123'
+ dsn: "file:/tmp/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2&_journal_mode=WAL"
+ 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}'
+ clientHost: "127.0.0.1:3000"
+ mailHost: "${MAIL_HOST}"
+ publicName: "LOCAL.SAYA.UZ"
+ mailAddress: "${MAIL_ADDRESS}"
+ username: "${MAIL_USERNAME}"
+ password: "${MAIL_PASSWORD}"
+ salt: "${MAIL_SALT}"
trigger:
onNewPost: "* * * * *"
-canonicalEndpoint: 'http://127.0.0.1:3000'
+canonicalEndpoint: "http://127.0.0.1:3000"
photoStorage:
full:
baseUrl: https://storage.yandexcloud.kz/sayauz-photos/%s
@@ -63,7 +71,7 @@ photoStorage:
baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp
homePageGifs:
baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif
- indexes: ['1', '2', '3']
+ indexes: ["1", "2", "3"]
staticStorage:
baseUrl: https://storage.yandexcloud.kz/sayauz-static
allowOrigins:
diff --git a/config/config.prod.yaml b/config/config.prod.yaml
index 868e242..e5785f8 100644
--- a/config/config.prod.yaml
+++ b/config/config.prod.yaml
@@ -1,23 +1,32 @@
logLevel: info
+endpoint:
+ type: unix
+ config:
+ path: "/etc/sayauz/prod.socket"
+ chmod: '0666'
blogPages:
storage:
- type: b2
+ type: s3
config:
- bucketName: sayana-pages
- region: eu-central-003
- prefix: 'prod-'
- keyID: '${B2_KEY_ID}'
- applicationKey: '${B2_APPLICATION_KEY}'
+ bucketName: sayauz-pages
+ region: kz1
+ prefix: "prod-"
+ endpoint: "https://storage.yandexcloud.kz"
+ usePathStyle: true
+ accessKeyID: "${S3_ACCESS_KEY_ID}"
+ secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
factGiver:
storage:
- type: b2
+ type: s3
config:
- bucketName: sayana-pages
- region: eu-central-003
- prefix: ''
- keyID: '${B2_KEY_ID}'
- applicationKey: '${B2_APPLICATION_KEY}'
- factsFileName: 'facts-*.txt'
+ bucketName: sayauz-pages
+ region: kz1
+ prefix: ""
+ endpoint: "https://storage.yandexcloud.kz"
+ usePathStyle: true
+ accessKeyID: "${S3_ACCESS_KEY_ID}"
+ secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
+ factsFileName: "facts-*.txt"
localePath: ./locale/
availableLanguages:
- name: ru
@@ -32,21 +41,22 @@ 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'
- salt: '${AUTH_SALT}'
+ dsn: "file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2&_journal_mode=WAL"
+ salt: "${AUTH_SALT}"
mail:
- clientHost: '${FQDN}'
- mailHost: '${MAIL_HOST}'
- publicName: 'SAYA.TODAY'
- mailAddress: '${MAIL_ADDRESS}'
- username: '${MAIL_USERNAME}'
- password: '${MAIL_PASSWORD}'
- salt: '${MAIL_SALT}'
+ clientHost: "${FQDN}"
+ mailHost: "${MAIL_HOST}"
+ publicName: "SAYA.UZ"
+ mailAddress: "${MAIL_ADDRESS}"
+ username: "${MAIL_USERNAME}"
+ password: "${MAIL_PASSWORD}"
+ salt: "${MAIL_SALT}"
trigger:
onNewPost: "0/5 * * * *"
-canonicalEndpoint: 'https://${FQDN}'
+canonicalEndpoint: "https://${FQDN}"
meta:
- googleSiteVerification: '${GOOGLE_SITE_VERIFICATION}'
+ googleSiteVerification: "${GOOGLE_SITE_VERIFICATION}"
+ yandexVerification: "${YANDEX_VERIFICATION}"
photoStorage:
full:
baseUrl: https://storage.yandexcloud.kz/sayauz-photos/%s
@@ -64,7 +74,7 @@ photoStorage:
baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp
homePageGifs:
baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif
- indexes: ['1', '2', '3']
+ indexes: ["1", "2", "3"]
staticStorage:
baseUrl: https://storage.yandexcloud.kz/sayauz-static
allowOrigins:
diff --git a/config/config.stage.yaml b/config/config.stage.yaml
index ab86709..c2e337b 100644
--- a/config/config.stage.yaml
+++ b/config/config.stage.yaml
@@ -1,23 +1,32 @@
logLevel: debug
+endpoint:
+ type: unix
+ config:
+ path: "/etc/sayauz/stage.socket"
+ chmod: '0666'
blogPages:
storage:
- type: b2
+ type: s3
config:
- bucketName: sayana-pages
- region: eu-central-003
- prefix: 'stage-'
- keyID: '${B2_KEY_ID}'
- applicationKey: '${B2_APPLICATION_KEY}'
+ bucketName: sayauz-pages
+ region: kz1
+ prefix: "stage-"
+ endpoint: "https://storage.yandexcloud.kz"
+ usePathStyle: true
+ accessKeyID: "${S3_ACCESS_KEY_ID}"
+ secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
factGiver:
storage:
- type: b2
+ type: s3
config:
- bucketName: sayana-pages
- region: eu-central-003
- prefix: ''
- keyID: '${B2_KEY_ID}'
- applicationKey: '${B2_APPLICATION_KEY}'
- factsFileName: 'facts-*.txt'
+ bucketName: sayauz-pages
+ region: kz1
+ prefix: ""
+ endpoint: "https://storage.yandexcloud.kz"
+ usePathStyle: true
+ accessKeyID: "${S3_ACCESS_KEY_ID}"
+ secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
+ factsFileName: "facts-*.txt"
localePath: ./locale/
availableLanguages:
- name: ru
@@ -32,19 +41,19 @@ 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'
- salt: '${AUTH_SALT}'
+ dsn: "file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2&_journal_mode=WAL"
+ 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}'
+ clientHost: "${FQDN}"
+ mailHost: "${MAIL_HOST}"
+ publicName: "STAGE.SAYA.UZ"
+ mailAddress: "${MAIL_ADDRESS}"
+ username: "${MAIL_USERNAME}"
+ password: "${MAIL_PASSWORD}"
+ salt: "${MAIL_SALT}"
trigger:
onNewPost: "0/3 * * * *"
-canonicalEndpoint: 'https://${FQDN}'
+canonicalEndpoint: "https://${FQDN}"
photoStorage:
full:
baseUrl: https://storage.yandexcloud.kz/sayauz-photos/%s
@@ -62,7 +71,7 @@ photoStorage:
baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp
homePageGifs:
baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif
- indexes: ['1', '2', '3']
+ indexes: ["1", "2", "3"]
staticStorage:
baseUrl: https://storage.yandexcloud.kz/sayauz-static
allowOrigins:
diff --git a/deploy/inventory.yml b/deploy/inventory.yml
index d3acab8..0a63140 100644
--- a/deploy/inventory.yml
+++ b/deploy/inventory.yml
@@ -8,6 +8,7 @@ all:
saya_today_web_hostname: sayauz-prod
saya_today_web_ipv4_address: 172.16.0.17
saya_today_web_listen_address: saya.uz
+ saya_today_web_unix_sockets_path: '/etc/sayauz'
stage:
hosts:
@@ -17,3 +18,4 @@ all:
saya_today_web_hostname: sayauz-stage
saya_today_web_ipv4_address: 172.16.0.18
saya_today_web_listen_address: stage.saya.uz
+ saya_today_web_unix_sockets_path: '/etc/sayauz'
diff --git a/deploy/playbook.yml b/deploy/playbook.yml
index 8fb9088..ae3e936 100644
--- a/deploy/playbook.yml
+++ b/deploy/playbook.yml
@@ -2,9 +2,27 @@
- name: Deploy saya-today-web Docker Container
hosts: all
remote_user: svc_github
+
+ vars:
+ docker_volumes:
+ - "{{ saya_today_web_name }}-volume:/data:rw"
tasks:
- - name: Create a volume
+ - name: Initialize Unix socket volume
+ when: >-
+ saya_today_web_unix_sockets_path is defined and
+ saya_today_web_unix_sockets_path is string and
+ saya_today_web_unix_sockets_path | length > 0
+ block:
+ - name: Ensure Unix socket volume exists
+ community.docker.docker_volume:
+ name: "sayana_sockets"
+ - name: Include Unix socket volume in docker volumes list
+ ansible.builtin.set_fact:
+ docker_volumes: >-
+ {{ docker_volumes + ["sayana_sockets:" ~ saya_today_web_unix_sockets_path ~ ":rw"] }}
+
+ - name: Create a data volume
community.docker.docker_volume:
name: "{{ saya_today_web_name }}-volume"
@@ -14,8 +32,8 @@
hostname: "{{ saya_today_web_hostname }}"
image: "ghcr.io/sayaandy/saya-today-web:{{ saya_today_web_tag }}"
env:
- B2_KEY_ID: "{{ saya_today_web_b2_key_id }}"
- B2_APPLICATION_KEY: "{{ saya_today_web_b2_application_key }}"
+ S3_ACCESS_KEY_ID: "{{ saya_today_web_s3_access_key_id }}"
+ S3_SECRET_ACCESS_KEY: "{{ saya_today_web_s3_secret_access_key }}"
ENVIRONMENT: "{{ saya_today_web_environment }}"
AUTH_SALT: "{{ saya_today_web_auth_salt }}"
MAIL_HOST: "{{ saya_today_web_mail_host }}"
@@ -25,12 +43,12 @@
MAIL_SALT: "{{ saya_today_web_mail_salt }}"
FQDN: "{{ saya_today_web_listen_address }}"
GOOGLE_SITE_VERIFICATION: "{{ saya_today_google_site_verification | default('') }}"
+ YANDEX_VERIFICATION: "{{ saya_today_yandex_verification | default('') }}"
network_mode: caddy
networks:
- name: caddy
ipv4_address: "{{ saya_today_web_ipv4_address }}"
- volumes:
- - "{{ saya_today_web_name }}-volume:/data:rw"
+ volumes: "{{ docker_volumes }}"
restart_policy: unless-stopped
no_log: true
...
diff --git a/go.mod b/go.mod
index c401c7a..779e641 100644
--- a/go.mod
+++ b/go.mod
@@ -5,44 +5,43 @@ go 1.26
require (
github.com/Backblaze/blazer v0.7.2
github.com/dgraph-io/ristretto/v2 v2.4.0
- github.com/go-co-op/gocron/v2 v2.20.0
+ github.com/go-co-op/gocron/v2 v2.21.1
github.com/go-playground/validator/v10 v10.30.2
- github.com/gofiber/fiber/v2 v2.52.12
+ github.com/gofiber/fiber/v2 v2.52.13
github.com/golang-migrate/migrate/v4 v4.19.1
- github.com/mattn/go-sqlite3 v1.14.41
+ github.com/mattn/go-sqlite3 v1.14.44
github.com/valyala/fasthttp v1.70.0
github.com/wneessen/go-mail v0.7.2
github.com/yuin/goldmark v1.8.2
- golang.org/x/crypto v0.49.0
+ golang.org/x/crypto v0.50.0
gopkg.in/yaml.v3 v3.0.1
)
require (
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
- github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
- github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
- github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
- github.com/tinylib/msgp v1.2.5 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
+ github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect
+ github.com/philhofer/fwd v1.2.0 // indirect
+ github.com/tinylib/msgp v1.6.4 // indirect
)
require (
github.com/andybalholm/brotli v1.2.1 // indirect
- github.com/aws/aws-sdk-go-v2 v1.41.5
- github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
- github.com/aws/aws-sdk-go-v2/config v1.32.14
- github.com/aws/aws-sdk-go-v2/credentials v1.19.14
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
- github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
- github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0
- github.com/aws/smithy-go v1.24.2 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.41.7
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.32.17
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.16
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1
+ github.com/aws/smithy-go v1.25.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -51,14 +50,14 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
- github.com/klauspost/compress v1.18.5 // indirect
+ github.com/klauspost/compress v1.18.6 // indirect
github.com/kr/pretty v0.3.1 // 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.22 // indirect
+ github.com/mattn/go-isatty v0.0.22 // indirect
+ github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
- golang.org/x/sys v0.42.0 // indirect
- golang.org/x/text v0.35.0 // indirect
+ golang.org/x/sys v0.43.0 // indirect
+ golang.org/x/text v0.36.0 // indirect
)
diff --git a/go.sum b/go.sum
index d0c2921..d277091 100644
--- a/go.sum
+++ b/go.sum
@@ -2,44 +2,42 @@ github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQ
github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
-github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
-github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
-github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI=
-github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
-github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
-github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
-github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
-github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
-github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
-github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
+github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
+github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
+github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
+github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1 h1:mxuT1xE+dI54NW3RkNjP8DUT5HXqbkiAFvfdyDFwE5c=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
+github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
+github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
+github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
+github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
@@ -55,8 +53,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
-github.com/go-co-op/gocron/v2 v2.20.0 h1:9IMrnnVSWjfSh3E54gWmWCHbloQJLh6f9+nwyKfLNpc=
-github.com/go-co-op/gocron/v2 v2.20.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
+github.com/go-co-op/gocron/v2 v2.21.1 h1:QYOK6iOQVCut+jDcs4zRdWRTBHRxRCEeeFi1TnAmgbU=
+github.com/go-co-op/gocron/v2 v2.21.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
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=
@@ -65,16 +63,16 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
-github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
-github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
+github.com/gofiber/fiber/v2 v2.52.13 h1:TOKP64iqC9b5P49VrBW5tHhUOvDyrtJ0xePEfzJbCbk=
+github.com/gofiber/fiber/v2 v2.52.13/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
-github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
+github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -85,14 +83,14 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.22 h1:76lXsPn6FyHtTY+jt2fTTvsMUCZq1k0qwRsAMuxzKAk=
-github.com/mattn/go-runewidth v0.0.22/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
-github.com/mattn/go-sqlite3 v1.14.41 h1:8p7Pwz5NHkEbWSqc/ygU4CBGubhFFkpgP9KwcdkAHNA=
-github.com/mattn/go-sqlite3 v1.14.41/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
-github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
-github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
+github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
+github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
+github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
+github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
+github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
+github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -102,8 +100,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
-github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
+github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
+github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=
@@ -116,13 +114,12 @@ github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
-golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
-golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
+golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
+golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/internal/blog/index.go b/internal/blog/index.go
new file mode 100644
index 0000000..861a90e
--- /dev/null
+++ b/internal/blog/index.go
@@ -0,0 +1,32 @@
+package blog
+
+import "time"
+
+const IndexFileName = "index.json"
+
+const IndexSchemaVersion = 1
+
+type IndexEntry struct {
+ Link string `json:"link"`
+ ModifiedTime time.Time `json:"modifiedTime"`
+ Title string `json:"title"`
+ ShortDescription string `json:"shortDescription"`
+ ActionDate string `json:"actionDate"`
+ PublishedTime time.Time `json:"publishedTime"`
+ Thumbnail string `json:"thumbnail"`
+ Tags []string `json:"tags"`
+ Geolocation string `json:"geolocation"`
+ Medley string `json:"medley,omitempty"`
+ MedleyPart int `json:"medleyPart,omitempty"`
+}
+
+type IndexCategory struct {
+ GeneratedAt time.Time `json:"generatedAt"`
+ Pages []IndexEntry `json:"pages"`
+}
+
+type Index struct {
+ SchemaVersion int `json:"schemaVersion"`
+ GeneratedAt time.Time `json:"generatedAt"`
+ Categories map[string]IndexCategory `json:"categories"`
+}
diff --git a/internal/blog/s3.go b/internal/blog/s3.go
index 86a6463..bbd5238 100644
--- a/internal/blog/s3.go
+++ b/internal/blog/s3.go
@@ -2,10 +2,15 @@ package blog
import (
"context"
+ "encoding/json"
+ "errors"
"fmt"
"io"
+ "log/slog"
+ "net/url"
"slices"
"strings"
+ "sync"
"time"
"github.com/SayaAndy/saya-today-web/config"
@@ -14,8 +19,11 @@ import (
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
+ s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
)
+const s3ScanConcurrency = 32
+
type S3Client struct {
prefix string
bucketName string
@@ -50,6 +58,7 @@ func NewS3Client(cfg *config.StorageConfig) (Client, error) {
}
s3Opts = append(s3Opts, func(o *s3.Options) {
o.UsePathStyle = s3cfg.UsePathStyle
+ o.DisableLogOutputChecksumValidationSkipped = true
})
s3cl := s3.NewFromConfig(awsCfg, s3Opts...)
@@ -58,79 +67,195 @@ func NewS3Client(cfg *config.StorageConfig) (Client, error) {
}
func (c *S3Client) Scan(prefix string) ([]*Page, error) {
- pages := []*Page{}
+ pages, err := c.scanFromIndex(prefix)
+ if err == nil {
+ return pages, nil
+ }
- fullPrefix := c.prefix + prefix
- input := &s3.ListObjectsV2Input{
+ var nsk *s3types.NoSuchKey
+ if !errors.As(err, &nsk) {
+ return nil, err
+ }
+
+ slog.Warn("index.json missing, falling back to listing", slog.String("prefix", c.prefix))
+ return c.scanByListing(prefix)
+}
+
+func (c *S3Client) scanFromIndex(prefix string) ([]*Page, error) {
+ out, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{
Bucket: aws.String(c.bucketName),
- Prefix: aws.String(fullPrefix),
+ Key: aws.String(IndexFileName),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("get index.json: %w", err)
+ }
+ defer out.Body.Close()
+
+ raw, err := io.ReadAll(out.Body)
+ if err != nil {
+ return nil, fmt.Errorf("read index.json: %w", err)
+ }
+
+ var idx Index
+ if err := json.Unmarshal(raw, &idx); err != nil {
+ return nil, fmt.Errorf("unmarshal index.json: %w", err)
+ }
+
+ wantLang := ""
+ if i := strings.Index(prefix, "/"); i > 0 {
+ wantLang = prefix[:i]
+ }
+
+ fullPrefix := c.prefix + prefix
+ pages := make([]*Page, 0)
+ for catKey, cat := range idx.Categories {
+ lang, ok := strings.CutPrefix(catKey, c.prefix)
+ if !ok {
+ continue
+ }
+ if wantLang != "" && wantLang != lang {
+ continue
+ }
+ for _, e := range cat.Pages {
+ if !strings.HasPrefix(e.Link, fullPrefix) {
+ continue
+ }
+ linkParts := strings.Split(e.Link, "/")
+ nameParts := strings.Split(linkParts[len(linkParts)-1], ".")
+ fileName := strings.Join(nameParts[:len(nameParts)-1], ".")
+ pages = append(pages, &Page{
+ Link: e.Link,
+ FileName: fileName,
+ Lang: lang,
+ ModifiedTime: e.ModifiedTime,
+ Metadata: &frontmatter.Metadata{
+ Title: e.Title,
+ ShortDescription: e.ShortDescription,
+ ActionDate: e.ActionDate,
+ PublishedTime: e.PublishedTime,
+ Thumbnail: e.Thumbnail,
+ Tags: e.Tags,
+ Geolocation: e.Geolocation,
+ Medley: e.Medley,
+ MedleyPart: e.MedleyPart,
+ },
+ })
+ }
}
+ return pages, nil
+}
- paginator := s3.NewListObjectsV2Paginator(c.s3cl, input)
+func (c *S3Client) scanByListing(prefix string) ([]*Page, error) {
+ fullPrefix := c.prefix + prefix
+
+ type candidate struct {
+ key string
+ lastModified time.Time
+ }
+ var candidates []candidate
+ paginator := s3.NewListObjectsV2Paginator(c.s3cl, &s3.ListObjectsV2Input{
+ Bucket: aws.String(c.bucketName),
+ Prefix: aws.String(fullPrefix),
+ })
for paginator.HasMorePages() {
output, err := paginator.NextPage(context.Background())
if err != nil {
return nil, fmt.Errorf("list S3 objects: %w", err)
}
-
for _, obj := range output.Contents {
key := aws.ToString(obj.Key)
-
if !strings.HasSuffix(key, ".md") {
continue
}
+ candidates = append(candidates, candidate{key, aws.ToTime(obj.LastModified)})
+ }
+ }
+
+ pages := make([]*Page, len(candidates))
+ sem := make(chan struct{}, s3ScanConcurrency)
+ var wg sync.WaitGroup
+ var firstErr error
+ var errMu sync.Mutex
+
+ for i, cand := range candidates {
+ sem <- struct{}{}
+ wg.Go(func() {
+ defer func() { <-sem }()
head, err := c.s3cl.HeadObject(context.Background(), &s3.HeadObjectInput{
Bucket: aws.String(c.bucketName),
- Key: aws.String(key),
+ Key: aws.String(cand.key),
})
if err != nil {
- return nil, fmt.Errorf("head S3 object %s: %w", key, err)
+ errMu.Lock()
+ if firstErr == nil {
+ firstErr = fmt.Errorf("head S3 object %s: %w", cand.key, err)
+ }
+ errMu.Unlock()
+ return
}
if head.ContentType == nil || !strings.Contains(*head.ContentType, "text/markdown") {
- continue
+ return
}
-
meta := head.Metadata
if meta["title"] == "" {
- continue
+ return
}
publishedTime, err := time.Parse(time.RFC3339, meta["published-time"])
if err != nil {
- return nil, fmt.Errorf("failed to parse published time metadata field: %w", err)
+ errMu.Lock()
+ if firstErr == nil {
+ firstErr = fmt.Errorf("failed to parse published time metadata field: %w", err)
+ }
+ errMu.Unlock()
+ return
}
- linkParts := strings.Split(key, "/")
+ linkParts := strings.Split(cand.key, "/")
nameParts := strings.Split(linkParts[len(linkParts)-1], ".")
fileName := strings.Join(nameParts[:len(nameParts)-1], ".")
+ lang, _ := strings.CutPrefix(linkParts[0], c.prefix)
tags := strings.Split(meta["tags"], ",")
slices.Sort(tags)
- lang, _ := strings.CutPrefix(linkParts[0], c.prefix)
+ title, _ := url.QueryUnescape(meta["title"])
+ shortDescription, _ := url.QueryUnescape(meta["short-description"])
+ thumbnail, _ := url.QueryUnescape(meta["thumbnail"])
- pages = append(pages, &Page{
- Link: key,
+ pages[i] = &Page{
+ Link: cand.key,
FileName: fileName,
Lang: lang,
- ModifiedTime: aws.ToTime(obj.LastModified),
+ ModifiedTime: cand.lastModified,
Metadata: &frontmatter.Metadata{
- Title: meta["title"],
- ShortDescription: meta["short-description"],
+ Title: title,
+ ShortDescription: shortDescription,
ActionDate: meta["action-date"],
PublishedTime: publishedTime,
- Thumbnail: meta["thumbnail"],
+ Thumbnail: thumbnail,
Tags: tags,
Geolocation: meta["geolocation"],
},
- })
- }
+ }
+ })
}
+ wg.Wait()
- return pages, nil
+ if firstErr != nil {
+ return nil, firstErr
+ }
+
+ out := pages[:0]
+ for _, p := range pages {
+ if p != nil {
+ out = append(out, p)
+ }
+ }
+ return out, nil
}
func (c *S3Client) ReadAll(path string) ([]byte, error) {
diff --git a/internal/router/handlers/root.go b/internal/router/handlers/root.go
index a8890fa..6a011e4 100644
--- a/internal/router/handlers/root.go
+++ b/internal/router/handlers/root.go
@@ -50,6 +50,9 @@ func (r *RootHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lan
if supplements.Meta.GoogleSiteVerification != "" {
meta = append(meta, router.MetaField{Name: "google-site-verification", Content: supplements.Meta.GoogleSiteVerification})
}
+ if supplements.Meta.YandexVerification != "" {
+ meta = append(meta, router.MetaField{Name: "yandex-verification", Content: supplements.Meta.YandexVerification})
+ }
return meta, nil
}
diff --git a/internal/router/router.go b/internal/router/router.go
index 5230e74..65e35a4 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -7,8 +7,11 @@ import (
"fmt"
"html/template"
"log/slog"
+ "net"
"net/url"
+ "os"
"slices"
+ "strconv"
"strings"
"time"
@@ -112,6 +115,7 @@ type Router struct {
templatedRoutes map[string]map[string]Route
templatedPathMatcher *PathMatcher
canonicalEndpoint string
+ endpoint config.EndpointConfig
}
func NewRouter(cfg *config.Config) (*Router, error) {
@@ -256,7 +260,7 @@ func NewRouter(cfg *config.Config) (*Router, error) {
templatedRoutes := make(map[string]map[string]Route)
templatedPathMatcher := NewPathMatcher()
- return &Router{supplements, app, templatedRoutes, templatedPathMatcher, cfg.CanonicalEndpoint}, nil
+ return &Router{supplements, app, templatedRoutes, templatedPathMatcher, cfg.CanonicalEndpoint, cfg.Endpoint}, nil
}
func (r *Router) InitRoutes() (err error) {
@@ -305,6 +309,8 @@ func (r *Router) InitRoutes() (err error) {
cacheKey = fmt.Sprintf("%s.full-page.%s", method, trimmedPath)
case ByUrlAndQuery:
cacheKey = fmt.Sprintf("%s.full-page.%s.%s", method, trimmedPath, queryString)
+ case Disabled:
+ c.Set("Cache-Control", "no-store, no-cache, must-revalidate")
}
defaultMap := fiber.Map{
@@ -367,6 +373,7 @@ func (r *Router) InitRoutes() (err error) {
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("unknown segment '%s'", part))
}
+ c.Set("Cache-Control", "no-store, no-cache, must-revalidate")
err := r.generalPageSegment(c, part)
return err
})
@@ -382,10 +389,30 @@ func (r *Router) InitRoutes() (err error) {
return nil
}
-func (r *Router) Listen(endpoint string) error {
- if err := r.app.Listen(endpoint); err != nil {
- return fmt.Errorf("error while running fiber server: %w", err)
+func (r *Router) Listen() error {
+ switch r.endpoint.Type {
+ case "unix":
+ unixConfig := r.endpoint.Config.(*config.UnixConfig)
+ endpoint, _ := strings.CutPrefix(unixConfig.Path, "unix://")
+ ln, err := net.Listen("unix", endpoint)
+ if err != nil {
+ return fmt.Errorf("error while initializing unix listener: %w", err)
+ }
+ chmod, _ := strconv.ParseUint(unixConfig.Chmod[1:], 8, 32)
+ os.Chmod(unixConfig.Path, os.FileMode(chmod))
+ if err := r.app.Listener(ln); err != nil {
+ return fmt.Errorf("error while running fiber server: %w", err)
+ }
+ case "http":
+ httpConfig := r.endpoint.Config.(*config.HttpConfig)
+ fmt.Print(httpConfig)
+ if err := r.app.Listen(httpConfig.ListenOn); err != nil {
+ return fmt.Errorf("error while running fiber server: %w", err)
+ }
+ default:
+ return fmt.Errorf("error with initializing fiber server: invalid endpoint type (supported are unix and http)")
}
+
return nil
}
@@ -409,6 +436,7 @@ func (r *Router) Close() (err error) {
}
slog.Debug("closing page cache")
r.supplements.PageCache.Close()
+
return errors.Join(allErrors...)
}
diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml
index 9923881..10bcb94 100644
--- a/locale/localization.en.yaml
+++ b/locale/localization.en.yaml
@@ -1,77 +1,77 @@
-TagsLabel: 'Tags:'
+TagsLabel: "Tags:"
BlogSearch:
- Header: 'Blog Search'
+ Header: "Blog Search"
Description: "The Saya Blog posts' list"
- TagsHeader: 'Tags'
- OrderByHeader: 'Order by...'
- TitleOrdered: 'Title'
- ActionDateOrdered: 'Action Date'
- PublicationDateOrdered: 'Publication Date'
- ChooseAllTags: 'Choose All'
+ TagsHeader: "Tags"
+ OrderByHeader: "Order by..."
+ TitleOrdered: "Title"
+ ActionDateOrdered: "Action Date"
+ PublicationDateOrdered: "Publication Date"
+ ChooseAllTags: "Choose All"
GlobalMap:
- Header: 'Global Map'
+ Header: "Global Map"
Description: "Global map with all the tags of places I've been to"
HomePage:
- Header: 'Home Page'
- Contacts: 'Contacts'
- DidYouKnowThat: 'Did you know, that...'
- SidebarDescription: 'Describing the Sidebar'
- HomePageDescription: 'Home Page -- where we are right now!'
- BlogSearchDescription: 'Blog Search. You can find my travel notes there and, maybe, something else!'
- MarkerMapDescription: 'Map with markers of my travel notes -- a convinient way to find an interesting region and find related posts!'
- ThemeSwitchDescription: 'Theme Palette lets you change the theme of the site -- there are 2 dark and 2 bright themes currently!'
- Hymn1: 'We welcome you to Sayasite!'
+ Header: "Home Page"
+ Contacts: "Contacts"
+ DidYouKnowThat: "Did you know, that..."
+ SidebarDescription: "Describing the Sidebar"
+ HomePageDescription: "Home Page -- where we are right now!"
+ BlogSearchDescription: "Blog Search. You can find my travel notes there and, maybe, something else!"
+ MarkerMapDescription: "Map with markers of my travel notes -- a convinient way to find an interesting region and find related posts!"
+ ThemeSwitchDescription: "Theme Palette lets you change the theme of the site -- there are 2 dark and 2 bright themes currently!"
+ Hymn1: "We welcome you to Sayasite!"
Hymn2: "We've been expecting you!"
- Hymn3: 'You bring such joy in Sayasite!'
- Hymn4: 'No matter where you roam, know our love is true!~'
+ 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?'
+ 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.'
+ Success: "Cancelled your subscription successfully! We do hope we will engage you again someday."
Metadata:
- Published: 'Published'
- Action: 'Took place on'
+ 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{/}.'
+ 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:'
+ Subject: "Verification Link for SAYA.UZ 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 :)'
+ 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'
+ Subject: "A new post arrived at SAYA.UZ!"
+ Intro: "A new post came!"
+ CapturedOn: "Captured on"
UserProfile:
- Header: 'Personal Settings'
- Description: 'Set and verify your e-mail here, as well as subscribe to favorite blog tags'
- 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'
+ Header: "Personal Settings"
+ Description: "Set and verify your e-mail here, as well as subscribe to favorite blog tags"
+ 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.'
+ 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.ru.yaml b/locale/localization.ru.yaml
index 34e5387..17e1e41 100644
--- a/locale/localization.ru.yaml
+++ b/locale/localization.ru.yaml
@@ -1,77 +1,77 @@
-TagsLabel: 'Тэги:'
+TagsLabel: "Тэги:"
BlogSearch:
- Header: 'Поиск по блогу'
- Description: 'Каталог записей Saya Blog'
- TagsHeader: 'Тэги'
- OrderByHeader: 'Упорядочить по...'
- TitleOrdered: 'названию'
- ActionDateOrdered: 'дате действия'
- PublicationDateOrdered: 'времени публикации'
- ChooseAllTags: 'Выбрать все'
+ Header: "Поиск по блогу"
+ Description: "Каталог записей Saya Blog"
+ TagsHeader: "Тэги"
+ OrderByHeader: "Упорядочить по..."
+ TitleOrdered: "названию"
+ ActionDateOrdered: "дате действия"
+ PublicationDateOrdered: "времени публикации"
+ ChooseAllTags: "Выбрать все"
GlobalMap:
- Header: 'Глобальная карта'
- Description: 'Глобальная карта с метками мест, где я побывал'
+ Header: "Глобальная карта"
+ Description: "Глобальная карта с метками мест, где я побывал"
HomePage:
- Header: 'Домашняя страница'
- Contacts: 'Контакты'
- DidYouKnowThat: 'А вы знали, что...'
- SidebarDescription: 'Поясняем за сайдбар'
- HomePageDescription: 'Домашняя страница -- где мы сейчас и находимся!'
- BlogSearchDescription: 'Поиск по блогу. Там размещены мои путевые заметки и, может, что-то ещё!'
- MarkerMapDescription: 'Карта с маркерами моих путевых заметок, -- удобный способ найти интересный регион и перейти на заинтересовавшие посты!'
- ThemeSwitchDescription: 'Позволяет поменять тему сайта -- есть 2 светлые и 2 тёмные темы!'
- Hymn1: 'Приветствуем Вас на Саясайте!'
- Hymn2: 'Мы Вас столько ждали!'
- Hymn3: 'Столько радости от Вас на Саясайте!'
- Hymn4: 'Где бы Вы не были, знайте, -- наша любовь крепка!'
+ Header: "Домашняя страница"
+ Contacts: "Контакты"
+ DidYouKnowThat: "А вы знали, что..."
+ SidebarDescription: "Поясняем за сайдбар"
+ HomePageDescription: "Домашняя страница -- где мы сейчас и находимся!"
+ BlogSearchDescription: "Поиск по блогу. Там размещены мои путевые заметки и, может, что-то ещё!"
+ MarkerMapDescription: "Карта с маркерами моих путевых заметок, -- удобный способ найти интересный регион и перейти на заинтересовавшие посты!"
+ ThemeSwitchDescription: "Позволяет поменять тему сайта -- есть 2 светлые и 2 тёмные темы!"
+ Hymn1: "Приветствуем Вас на Саясайте!"
+ Hymn2: "Мы Вас столько ждали!"
+ Hymn3: "Столько радости от Вас на Саясайте!"
+ Hymn4: "Где бы Вы не были, знайте, -- наша любовь крепка!"
UnsubscribePage:
- Header: 'Отписка'
- UnsetCode: 'Код для отписки не проставлен. Вы случайно не заблудились?'
- InvalidCode: 'Код для отписки не подтверждён. Может, истекло время в сутки для кода. А может, вы уже успели отписаться.'
- OnServerError: 'Мы не смогли вас отписать, проблема на нашей стороне! В скором времени займёмся проблемой.'
- Success: 'Вы отписались от рассылки! Надеюсь, мы ещё сможем вас заинтересовать.'
+ Header: "Отписка"
+ UnsetCode: "Код для отписки не проставлен. Вы случайно не заблудились?"
+ InvalidCode: "Код для отписки не подтверждён. Может, истекло время в сутки для кода. А может, вы уже успели отписаться."
+ OnServerError: "Мы не смогли вас отписать, проблема на нашей стороне! В скором времени займёмся проблемой."
+ Success: "Вы отписались от рассылки! Надеюсь, мы ещё сможем вас заинтересовать."
Metadata:
- Published: 'Опубликовано'
- Action: 'Время действия'
+ Published: "Опубликовано"
+ Action: "Время действия"
Mail:
- UnsubscribeFooter: 'Если данное письмо пришло вам случайно, либо вы хотите отписаться, можете перейти по {}этой ссылке{/}.'
+ UnsubscribeFooter: "Если данное письмо пришло вам случайно, либо вы хотите отписаться, можете перейти по {}этой ссылке{/}."
VerifyEmail:
- Subject: 'Ссылка для верификации для подтверждения вашей почты SAYA.TODAY'
- Welcome: 'Приветствую!'
- Intro: 'Есть 2 способа, чтобы подтвердить свою электронную почту:'
- GotoLink: 'Перейти по этой ссылке:'
- InputCode: 'Либо ввести этот код на странице подписок:'
- IfRandom: 'Если ты не понимаешь, что это за спам, можешь проигнорировать это письмо, а ещё, если интересно, можешь посетить мой сайт :)'
+ Subject: "Ссылка для верификации для подтверждения вашей почты SAYA.UZ"
+ Welcome: "Приветствую!"
+ Intro: "Есть 2 способа, чтобы подтвердить свою электронную почту:"
+ GotoLink: "Перейти по этой ссылке:"
+ InputCode: "Либо ввести этот код на странице подписок:"
+ IfRandom: "Если ты не понимаешь, что это за спам, можешь проигнорировать это письмо, а ещё, если интересно, можешь посетить мой сайт :)"
NewPost:
- Subject: 'Новый пост на SAYA.TODAY!'
- Intro: 'Вышел новый пост!'
- CapturedOn: 'Снималось'
+ Subject: "Новый пост на SAYA.UZ!"
+ Intro: "Вышел новый пост!"
+ CapturedOn: "Снималось"
UserProfile:
- Header: 'Личные настройки'
- Description: 'Здесь можно настроить и верифицировать свою электронную почту, а также подписаться на избранные темы блога'
- EmailHeader: 'E-Mail'
- VerificationCodeHeader: 'Код верификации'
- TypeEmail: 'Пожалуйста, введите свою почту'
- SendCodeButton: 'Отправить код'
- VerifyButton: 'Верифицировать'
- VerificationCodeSent: 'Выслали вам код верификации! Проверьте свою почту. Следующая попытка верификации будет возможна {}.'
- DelayTilVerification: 'Вы уже запрашивали недавно верификацию! Дождитесь {} для новой попытки.'
- FailedEmailRender: 'Неудачная попытка создать электронное письмо, более подробно спрашивайте у администратора.'
- VerificationCodeSendingError: 'Неудачная попытка прислать код для верификации, более подробно спрашивайте у администратора.'
- EmailAlreadyValidated: 'Эту почту вы уже подтверждали.'
- EmailTaken: 'Эта почта уже занята.'
- EmailEmpty: 'Почта не введена.'
- VerificationSuccess: 'Поздравляем! Теперь по этой почте можно подписаться на блог.'
- VerificationFailed: 'Код для верификации невалиден, при вопросах, пожалуйста, обращайтесь к администратору.'
- VerificationEmpty: 'Код для верификации не введён.'
- SubscriptionHeader: 'Подписаться на...'
- TagsNone: 'Ничего'
- TagsAll: 'Всё'
- TagsSpecific: 'Определённое'
+ Header: "Личные настройки"
+ Description: "Здесь можно настроить и верифицировать свою электронную почту, а также подписаться на избранные темы блога"
+ 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: 'Пожалуйста, обновите страницу с профилем на устройстве, откуда запрашивали код.'
+ SaveButton: "Сохранить"
+ SubscribeInvalidType: "Неизвестный тип подписки."
+ FailedToSubscribe: "Неудачная попытка записать новую подписку, более подробно спрашивайте у администратора."
+ SubscribedSuccessfully: "Вы успешно подписались по новым настройкам!"
+ RefreshPage: "Пожалуйста, обновите страницу с профилем на устройстве, откуда запрашивали код."
diff --git a/main.go b/main.go
index 3691e5b..9d90916 100644
--- a/main.go
+++ b/main.go
@@ -47,7 +47,7 @@ func main() {
routerShutdown := make(chan struct{}, 1)
go func() {
- if err := app.Listen(":3000"); err != nil {
+ if err := app.Listen(); err != nil {
slog.Error("error while running fiber server", slog.String("error", err.Error()))
if nestedErr := app.Close(); nestedErr != nil {
slog.Error("error while shutting down fiber server", slog.String("error", err.Error()))
diff --git a/static/favicon-dark.svg b/static/favicon-dark.svg
new file mode 100644
index 0000000..f09a1bf
--- /dev/null
+++ b/static/favicon-dark.svg
@@ -0,0 +1,16 @@
+<svg role="img" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" id="Lutris--Streamline-Simple-Icons" height="120" width="120">
+ <desc>
+ Lutris Streamline Icon: https://streamlinehq.com
+ </desc>
+ <defs>
+ <linearGradient gradientTransform="rotate(150, 0.5, 0.5)" x1="50%" y1="0%" x2="50%" y2="100%" id="gradient">
+ <stop stop-color="hsl(31.4, 100%, 70.8%)" stop-opacity="1" offset="0%"></stop><stop stop-color="hsl(17, 100%, 31%)" stop-opacity="1" offset="100%"></stop>
+ </linearGradient>
+ <filter id="filter" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+ <feTurbulence type="fractalNoise" baseFrequency="0.005 0.003" numOctaves="1" seed="2" stitchTiles="stitch" x="0%" y="0%" width="100%" height="100%" result="turbulence"></feTurbulence>
+ <feGaussianBlur stdDeviation="20 0" x="0%" y="0%" width="100%" height="100%" in="turbulence" edgeMode="duplicate" result="blur"></feGaussianBlur>
+ <feBlend mode="color-dodge" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" in2="blur" result="blend"></feBlend>
+ </filter>
+ </defs>
+ <path d="m106.155 94.45 0.005 -0.01c-6.465 16.215 -26.09 26.16 -47.235 25.525C26.5 119.965 0 92.4 0 59.53S26.38 0.005 58.925 0.005c8.965 0 17.465 2.03 25.075 5.65 0.405 -0.885 1.355 -2.72 2.255 -2.785 1.19 -0.085 1.87 0.685 2.63 1.545 0.77 0.86 2.3 2.145 2.3 2.145s6.965 -2.405 14.775 1.885c7.815 4.29 8.915 5.58 10.45 8.58 0.76 1.505 0.975 4.145 1 6.41a3.98 3.98 0 0 0 -0.35 -0.015c-2.48 0 -4.8 2.275 -4.8 5.4 0 1.315 0.41 2.48 1.075 3.39l-0.05 0.035a7.525 7.525 0 0 0 -0.66 0.05 93.52 93.52 0 0 0 -1.945 -0.71 12.65 12.65 0 0 1 -4.1 -2.36 7.01 7.01 0 0 0 -5.98 -10.56c-1.915 0 -3.65 0.78 -4.91 2.05 -2.36 -1.355 -5.87 -2.41 -12.635 -2.825l-2.035 -0.055c-11.41 0.06 -18.055 1.395 -29.895 6.505 -3.015 1.415 -6.03 3.075 -8.925 5.005 -2.115 1.5 -3.195 3.35 -3.545 5.685a6.63 6.63 0 0 0 6.15 6.865h0.210c6.35 0.3 10.195 9.95 10.315 12.485 0.02 0.25 0.02 0.115 0.015 0.4 -0.16 3.635 -1.85 6.335 -5.44 6.23a6.155 6.155 0 0 1 -4.88 -2.47c-0.315 -0.385 -0.515 -0.86 -0.795 -1.27 -3.33 -5.405 -8.66 -6.80 -13.855 -7.615 -2.19 -0.34 -5.365 -0.61 -6.55 1.25a41.4 41.4 0 0 0 -2.885 15.315c-0.1 25.18 20.205 45.59 45.13 45.59 12.875 0 26.745 -4.76 34.965 -13.5l-0.175 0.15c-8.86 7.365 -23.3 9.705 -30.135 9.705 -21.51 0 -39.09 -16.16 -39.09 -37.89 0 -6.38 1.44 -11.98 4.07 -16.8 2.475 0.915 4.735 2.415 6.4 5.11a1.2 1.2 0 0 0 0.065 0.1050c0.32 0.46 0.555 0.985 0.91 1.42 2.12 2.62 4.405 3.29 6.71 3.4h0.05c2.15 0.065 3.84 -0.6 5.12 -1.71 1.735 -1.5 2.75 -3.95 2.885 -6.91v-0.07c0.01 -0.425 0 -0.265 -0.02 -0.56 -0.12 -1.88 -1.665 -6.59 -4.53 -10.135 -1.33 -1.655 -2.935 -3.035 -4.75 -3.87l0.6 -0.37c3.78 -2.285 11.82 -4.885 22.96 -3.19 5.65 0.865 10.275 2.095 17.415 4.395 8.285 2.67 12.895 6.395 19.27 7.135 0.75 0.085 1.505 0.09 2.25 0.015 2.05 5.645 3.17 11.75 3.17 18.105 0 10.34 -2.95 19.975 -8.055 28.1zm9.735 -61.37s-0.575 1.005 -1.82 1.61c-0.515 0.25 -1.41 -0.375 -2.25 0.5 -1.795 3.63 2.58 6.66 4.615 6.575 2.04 -0.085 3.65 -2.16 3.56 -3.965 -0.085 -2.79 -4.1 -4.72 -4.1 -4.72zm1.17 -7.16c1.275 0 2.31 1.3 2.31 2.9 0 1.6 -1.035 2.9 -2.31 2.9 -1.27 0 -2.3 -1.3 -2.3 -2.9 0 -1.6 1.03 -2.9 2.3 -2.9zm-16.46 -4.755c2.46 0 4.45 2.015 4.45 4.5a4.475 4.475 0 0 1 -4.45 4.49 4.475 4.475 0 0 1 -4.45 -4.495c0 -2.48 1.995 -4.495 4.45 -4.495z" fill="url(#gradient)" filter="url(#filter)" stroke-width="5"></path>
+</svg>
diff --git a/static/favicon-light.svg b/static/favicon-light.svg
new file mode 100644
index 0000000..e7ed6eb
--- /dev/null
+++ b/static/favicon-light.svg
@@ -0,0 +1,16 @@
+<svg role="img" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" id="Lutris--Streamline-Simple-Icons" height="120" width="120">
+ <desc>
+ Lutris Streamline Icon: https://streamlinehq.com
+ </desc>
+ <defs>
+ <linearGradient gradientTransform="rotate(150, 0.5, 0.5)" x1="50%" y1="0%" x2="50%" y2="100%" id="gradient">
+ <stop stop-color="hsl(30.1, 100%, 44.1%)" stop-opacity="1" offset="0%"></stop><stop stop-color="hsl(23.1, 90.7%, 25.3%)" stop-opacity="1" offset="100%"></stop>
+ </linearGradient>
+ <filter id="filter" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+ <feTurbulence type="fractalNoise" baseFrequency="0.005 0.003" numOctaves="1" seed="2" stitchTiles="stitch" x="0%" y="0%" width="100%" height="100%" result="turbulence"></feTurbulence>
+ <feGaussianBlur stdDeviation="20 0" x="0%" y="0%" width="100%" height="100%" in="turbulence" edgeMode="duplicate" result="blur"></feGaussianBlur>
+ <feBlend mode="color-dodge" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" in2="blur" result="blend"></feBlend>
+ </filter>
+ </defs>
+ <path d="m106.155 94.45 0.005 -0.01c-6.465 16.215 -26.09 26.16 -47.235 25.525C26.5 119.965 0 92.4 0 59.53S26.38 0.005 58.925 0.005c8.965 0 17.465 2.03 25.075 5.65 0.405 -0.885 1.355 -2.72 2.255 -2.785 1.19 -0.085 1.87 0.685 2.63 1.545 0.77 0.86 2.3 2.145 2.3 2.145s6.965 -2.405 14.775 1.885c7.815 4.29 8.915 5.58 10.45 8.58 0.76 1.505 0.975 4.145 1 6.41a3.98 3.98 0 0 0 -0.35 -0.015c-2.48 0 -4.8 2.275 -4.8 5.4 0 1.315 0.41 2.48 1.075 3.39l-0.05 0.035a7.525 7.525 0 0 0 -0.66 0.05 93.52 93.52 0 0 0 -1.945 -0.71 12.65 12.65 0 0 1 -4.1 -2.36 7.01 7.01 0 0 0 -5.98 -10.56c-1.915 0 -3.65 0.78 -4.91 2.05 -2.36 -1.355 -5.87 -2.41 -12.635 -2.825l-2.035 -0.055c-11.41 0.06 -18.055 1.395 -29.895 6.505 -3.015 1.415 -6.03 3.075 -8.925 5.005 -2.115 1.5 -3.195 3.35 -3.545 5.685a6.63 6.63 0 0 0 6.15 6.865h0.210c6.35 0.3 10.195 9.95 10.315 12.485 0.02 0.25 0.02 0.115 0.015 0.4 -0.16 3.635 -1.85 6.335 -5.44 6.23a6.155 6.155 0 0 1 -4.88 -2.47c-0.315 -0.385 -0.515 -0.86 -0.795 -1.27 -3.33 -5.405 -8.66 -6.80 -13.855 -7.615 -2.19 -0.34 -5.365 -0.61 -6.55 1.25a41.4 41.4 0 0 0 -2.885 15.315c-0.1 25.18 20.205 45.59 45.13 45.59 12.875 0 26.745 -4.76 34.965 -13.5l-0.175 0.15c-8.86 7.365 -23.3 9.705 -30.135 9.705 -21.51 0 -39.09 -16.16 -39.09 -37.89 0 -6.38 1.44 -11.98 4.07 -16.8 2.475 0.915 4.735 2.415 6.4 5.11a1.2 1.2 0 0 0 0.065 0.1050c0.32 0.46 0.555 0.985 0.91 1.42 2.12 2.62 4.405 3.29 6.71 3.4h0.05c2.15 0.065 3.84 -0.6 5.12 -1.71 1.735 -1.5 2.75 -3.95 2.885 -6.91v-0.07c0.01 -0.425 0 -0.265 -0.02 -0.56 -0.12 -1.88 -1.665 -6.59 -4.53 -10.135 -1.33 -1.655 -2.935 -3.035 -4.75 -3.87l0.6 -0.37c3.78 -2.285 11.82 -4.885 22.96 -3.19 5.65 0.865 10.275 2.095 17.415 4.395 8.285 2.67 12.895 6.395 19.27 7.135 0.75 0.085 1.505 0.09 2.25 0.015 2.05 5.645 3.17 11.75 3.17 18.105 0 10.34 -2.95 19.975 -8.055 28.1zm9.735 -61.37s-0.575 1.005 -1.82 1.61c-0.515 0.25 -1.41 -0.375 -2.25 0.5 -1.795 3.63 2.58 6.66 4.615 6.575 2.04 -0.085 3.65 -2.16 3.56 -3.965 -0.085 -2.79 -4.1 -4.72 -4.1 -4.72zm1.17 -7.16c1.275 0 2.31 1.3 2.31 2.9 0 1.6 -1.035 2.9 -2.31 2.9 -1.27 0 -2.3 -1.3 -2.3 -2.9 0 -1.6 1.03 -2.9 2.3 -2.9zm-16.46 -4.755c2.46 0 4.45 2.015 4.45 4.5a4.475 4.475 0 0 1 -4.45 4.49 4.475 4.475 0 0 1 -4.45 -4.495c0 -2.48 1.995 -4.495 4.45 -4.495z" fill="url(#gradient)" filter="url(#filter)" stroke-width="5"></path>
+</svg>
diff --git a/static/favicon.ico b/static/favicon.ico
deleted file mode 100644
index 33eb226..0000000
--- a/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/static/input.css b/static/input.css
index 75482d8..5d8de42 100644
--- a/static/input.css
+++ b/static/input.css
@@ -1,5 +1,3 @@
-@import url('https://fonts.googleapis.com/css2?family=Gentium+Plus:ital,wght@0,400;0,700;1,400;1,700&family=M+PLUS+Rounded+1c:wght@100;300;400;500;700;800;900&display=swap');
-
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@@ -48,7 +46,8 @@ html {
@utility bg-paper-* {
background-image: --value([image], [url]), var(--bg-paper);
background-size: --value("auto", "cover", "contain"), 128px;
- background-repeat: --value("repeat", "repeat-x", "repeat-y", "no-repeat"), repeat;
+ background-repeat:
+ --value("repeat", "repeat-x", "repeat-y", "no-repeat"), repeat;
background-blend-mode: --value("overlay", "multiply", "normal"), normal;
}
@@ -145,7 +144,9 @@ body {
--color-leaflet-text: var(--color-main-hard);
--color-leaflet-link: var(--color-main-medium);
--color-leaflet-link-hover: var(--color-main-soft);
- --interlocked-hexagons-background: url('https://storage.yandexcloud.kz/sayauz-static/themes/lettuce/linen-fabric.webp'), url('https://storage.yandexcloud.kz/sayauz-static/themes/lettuce/lettuce-tiles.svg');
+ --interlocked-hexagons-background:
+ url("https://storage.yandexcloud.kz/sayauz-static/themes/lettuce/linen-fabric.webp"),
+ url("https://storage.yandexcloud.kz/sayauz-static/themes/lettuce/lettuce-tiles.svg");
--interlocked-hexagons-background-repeat: repeat, repeat;
--interlocked-hexagons-background-size: 11dvh, 20dvh;
--squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23bbd095%22%2F%3E%3Cpath%20fill%3D%22%23f5ede1%22%20d%3D%22M0%200v6.77L6.72%200zm8.91%200L0%208.96v3.86L12.83%200zm6.11%200L0%2015.06v3.87L18.9%200zm6.11%200L0%2021.12v3.92L25%200zM25%200l25%2024.99v-3.87L28.87%200zm25%2024.99%2025%2024.98L100%2025%2075%200zm-50%20.05v3.87l21.13%2021.12H25l-25-25zm25%2024.99h3.87L50%2028.9v-3.87zm0%200L0%2075l25%2025%2025-24.99zM50%2075l25-24.98h-3.87L50%2071.14v3.87zm0%200v3.87L71.13%20100H75L50%2075.01zm25%2025h3.87L100%2078.88v-3.87zm25-24.99v-3.87L78.87%2050.03H75L100%2075zM31.1%200%2050%2018.93v-3.87L34.98%200h-3.87zm6.07%200L50%2012.82V8.96L41.04%200zm6.1%200L50%206.77V0h-6.72zM25%202.19%202.14%2025.04H25zm0%2022.85v22.8l22.81-22.8zM75%203.92l21.13%2021.12L75%2046.16%2053.87%2025.04zm0%202.19L56.06%2025.04l18.89%2018.88%2018.94-18.88L75%206.1zm0%203.92%2015.07%2015L75%2040.06%2059.98%2025.04zm0%202.18L62.17%2025.04%2075%2037.86l12.83-12.82L75%2012.2zm0%203.82%208.96%209L75%2033.95%2066.04%2025zm0%202.24-6.72%206.77L75%2031.76l6.77-6.72h-.05zM0%2031.09v3.87l15.02%2015.07h3.87zm50%200L31.1%2050.03h3.88L50%2034.96V31.1zM0%2037.2v3.87l8.91%208.96h3.92zm50%200L37.17%2050.03h3.87L50%2041.07zM0%2043.3v6.73h6.72zm50%200-6.72%206.73H50zm0%206.73v6.71l6.72-6.71zm8.96%200L50%2058.93v3.92l12.83-12.82zm6.06%200L50%2065.03v3.88l18.94-18.88zm16.09%200L100%2068.9v-3.87l-15.02-15zm6.11%200L100%2062.85v-3.92l-8.91-8.9zm6.06%200%206.72%206.71v-6.71zM75%2052.2%2052.19%2075H75zM75%2075v22.8L97.81%2075zM25%2053.9l21.13%2021.12L25%2096.13%203.87%2075.01zm0%202.2L6.1%2074.95l18.95%2018.88%2018.9-18.88L25%2056.08zm0%203.9%2015.02%2015.01L25%2090.03%209.98%2075zm0%202.19L12.17%2075%2025%2087.84%2037.83%2075%2025%2062.2zm0%203.87L33.96%2075%2025%2083.97%2016.04%2075zm0%202.23-6.72%206.72L25%2081.73l6.72-6.72L25%2068.3zm25%2012.83v3.87L65.02%20100h3.87zm50%200L81.1%20100h3.88L100%2084.99zm-50%206.06v3.86l8.96%208.96h3.87zm50%200L87.22%20100h3.87l8.91-8.96zm-50%206.1V100h6.72zm50%200L93.28%20100H100z%22%2F%3E%3C%2Fsvg%3E");
@@ -175,7 +176,44 @@ body {
--color-leaflet-text: var(--color-background-dark);
--color-leaflet-link: var(--color-background-dark);
--color-leaflet-link-hover: var(--color-background-light);
- --interlocked-hexagons-background: radial-gradient(33.5% 25% at 100% 82%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(54% 25% at 55% 100%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(37% 19.5% at 20% 100%, rgba(176, 220, 248, 0.3) 0%, rgba(197, 172, 247, 0.2) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(63% 34.5% at 66% 77%, rgba(190, 227, 248, 0.15) 0%, rgba(197, 179, 246, 0.1) 33.33%, rgba(255, 0, 0, 0) 100%), linear-gradient(0deg, #2f5883 0%, #2f4e7a 4.75%, #2e4472 9.5%, #2a3065 19%, #272e57 26.5%, #242b4b 34%, #1f2437 49%, #1c2030 57.75%, #1a1b29 66.5%, #13131b 84%);
+ --interlocked-hexagons-background:
+ radial-gradient(
+ 33.5% 25% at 100% 82%,
+ rgba(190, 227, 248, 0.2) 0%,
+ rgba(197, 179, 246, 0.13) 33.33%,
+ rgba(255, 0, 0, 0) 100%
+ ),
+ radial-gradient(
+ 54% 25% at 55% 100%,
+ rgba(190, 227, 248, 0.2) 0%,
+ rgba(197, 179, 246, 0.13) 33.33%,
+ rgba(255, 0, 0, 0) 100%
+ ),
+ radial-gradient(
+ 37% 19.5% at 20% 100%,
+ rgba(176, 220, 248, 0.3) 0%,
+ rgba(197, 172, 247, 0.2) 33.33%,
+ rgba(255, 0, 0, 0) 100%
+ ),
+ radial-gradient(
+ 63% 34.5% at 66% 77%,
+ rgba(190, 227, 248, 0.15) 0%,
+ rgba(197, 179, 246, 0.1) 33.33%,
+ rgba(255, 0, 0, 0) 100%
+ ),
+ linear-gradient(
+ 0deg,
+ #2f5883 0%,
+ #2f4e7a 4.75%,
+ #2e4472 9.5%,
+ #2a3065 19%,
+ #272e57 26.5%,
+ #242b4b 34%,
+ #1f2437 49%,
+ #1c2030 57.75%,
+ #1a1b29 66.5%,
+ #13131b 84%
+ );
--interlocked-hexagons-background-repeat: no-repeat;
--interlocked-hexagons-background-size: 100dvw 100dvh;
--squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2260%22%20height%3D%2260%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%2313131b%22%2F%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23363645%22%20stroke-width%3D%22.5%22%20d%3D%22M10%2050H0m60-40H50m0%2040v10M10%200v10m21%2030v20m0-60v20m9%2010h20M0%2030h20m0%2010h20V20H20v10zM10%2010h40v40H10zM0%200h60v60H0z%22%2F%3E%3C%2Fsvg%3E");
@@ -205,10 +243,10 @@ body {
--color-leaflet-text: var(--color-background-dark);
--color-leaflet-link: var(--color-background-dark);
--color-leaflet-link-hover: var(--color-background-light);
- --interlocked-hexagons-background: url('https://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-background.webp');
+ --interlocked-hexagons-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-background.webp");
--interlocked-hexagons-background-repeat: repeat;
--interlocked-hexagons-background-size: auto;
- --squares-and-triangles-background: url('https://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-sidebar.webp');
+ --squares-and-triangles-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-sidebar.webp");
--squares-and-triangles-background-repeat: repeat;
--squares-and-triangles-background-width-coef: 5;
--squares-and-triangles-background-height-coef: 15;
@@ -219,7 +257,7 @@ body {
--color-accent-deep: var(--color-indigo-800);
--color-main-dark-hard: #343b58;
--color-main-dark-medium: #414868;
- --color-main-dark-soft: #6c6e75;
+ --color-main-dark-soft: #6c6e75;
--color-main-light-hard: #e3e6ff;
--color-main-light-medium: #d1d6ff;
--color-main-light-soft: #c3caff;
@@ -242,16 +280,18 @@ body {
--squares-and-triangles-background-repeat: repeat;
--squares-and-triangles-background-width-coef: 5;
--squares-and-triangles-background-height-coef: 5;
- --split-left-background: url('https://storage.yandexcloud.kz/sayauz-static/themes/lilac/split-left.webp');
- --split-right-background: url('https://storage.yandexcloud.kz/sayauz-static/themes/lilac/split-right.webp');
+ --split-left-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/lilac/split-left.webp");
+ --split-right-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/lilac/split-right.webp");
}
-a, .linklike {
+a,
+.linklike {
color: var(--color-main-medium) !important;
text-decoration: underline;
}
-a:hover, .linklike:hover {
+a:hover,
+.linklike:hover {
color: var(--color-main-soft) !important;
}
@@ -271,7 +311,7 @@ a:hover, .linklike:hover {
.crossable.disabled::before,
.crossable.htmx-request::before {
- content: '';
+ content: "";
position: absolute;
top: 0;
left: 0;
@@ -317,10 +357,18 @@ form.crossable.htmx-request input {
.text-sidebar {
color: var(--color-main-dark-soft);
- text-shadow: -0.2rem 0 var(--color-main-dark-hard), 0 -0.2rem var(--color-main-dark-hard), 0.2rem 0rem var(--color-main-light-soft), 0 0.2rem var(--color-main-light-soft);
+ text-shadow:
+ -0.2rem 0 var(--color-main-dark-hard),
+ 0 -0.2rem var(--color-main-dark-hard),
+ 0.2rem 0rem var(--color-main-light-soft),
+ 0 0.2rem var(--color-main-light-soft);
-webkit-text-stroke-color: var(--color-sidebar-stroke);
-webkit-text-stroke-width: 0.1rem;
- background: linear-gradient(135deg, color-mix(in srgb, var(--color-main-light-soft) 20%, transparent), color-mix(in srgb, var(--color-main-dark-soft) 20%, transparent));
+ background: linear-gradient(
+ 135deg,
+ color-mix(in srgb, var(--color-main-light-soft) 20%, transparent),
+ color-mix(in srgb, var(--color-main-dark-soft) 20%, transparent)
+ );
}
.text-secondary {
@@ -338,11 +386,15 @@ form.crossable.htmx-request input {
.bg-sidebar {
background-image: var(--squares-and-triangles-background);
background-repeat: var(--squares-and-triangles-background-repeat);
- background-size: calc(var(--squares-and-triangles-background-width-coef) * 1rem) calc(var(--squares-and-triangles-background-height-coef) * 1rem);
+ background-size: calc(
+ var(--squares-and-triangles-background-width-coef) * 1rem
+ )
+ calc(var(--squares-and-triangles-background-height-coef) * 1rem);
}
.inset-shadow {
- box-shadow: inset 0dvh 1dvh 1dvh -1dvh rgba(75, 81, 58, 0.5),
+ box-shadow:
+ inset 0dvh 1dvh 1dvh -1dvh rgba(75, 81, 58, 0.5),
inset 1dvh 0dvh 1dvh -1dvh rgba(75, 81, 58, 0.5),
inset -1dvh 0dvh 1dvh -1dvh rgba(75, 81, 58, 0.5),
inset 0dvh -0.5dvh rgba(75, 81, 58, 0.5);
@@ -404,7 +456,7 @@ form.crossable.htmx-request input {
}
.theme-icon.active::after {
- content: '';
+ content: "";
position: absolute;
top: 0;
left: 0;
@@ -419,7 +471,8 @@ form.crossable.htmx-request input {
position: relative;
background: color-mix(in srgb, var(--color-main-soft) 75%, transparent);
box-shadow: 0rem 0rem 1rem var(--color-main-hard);
- border: 0.5rem solid color-mix(in srgb, var(--color-main-soft) 90%, transparent);
+ border: 0.5rem solid
+ color-mix(in srgb, var(--color-main-soft) 90%, transparent);
backdrop-filter: blur(2px);
top: 35%;
left: 35%;
@@ -435,7 +488,11 @@ form.crossable.htmx-request input {
left: 0;
width: 100%;
height: 100%;
- background: linear-gradient(135deg, var(--color-main-light-hard), var(--color-main-dark-hard));
+ background: linear-gradient(
+ 135deg,
+ var(--color-main-light-hard),
+ var(--color-main-dark-hard)
+ );
opacity: 0.2;
z-index: 10;
content: "";
@@ -458,14 +515,16 @@ form.crossable.htmx-request input {
--border-size: 0.5rem;
}
-.themed-button:active, .themed-button.active {
+.themed-button:active,
+.themed-button.active {
border-top: var(--border-size) solid var(--color-main-dark-hard);
border-left: var(--border-size) solid var(--color-main-dark-hard);
border-bottom: var(--border-size) solid var(--color-main-light-hard);
border-right: var(--border-size) solid var(--color-main-light-hard);
}
-.themed-button:active > *, .themed-button.active > * {
+.themed-button:active > *,
+.themed-button.active > * {
transform: translateY(calc(var(--border-size) / 2));
}
@@ -473,7 +532,10 @@ form.crossable.htmx-request input {
--border-size: 0.25rem;
background-image: var(--squares-and-triangles-background);
background-repeat: var(--squares-and-triangles-background-repeat);
- background-size: calc(var(--squares-and-triangles-background-width-coef) * 0.9rem) calc(var(--squares-and-triangles-background-height-coef) * 0.9rem);
+ background-size: calc(
+ var(--squares-and-triangles-background-width-coef) * 0.9rem
+ )
+ calc(var(--squares-and-triangles-background-height-coef) * 0.9rem);
}
.themed-button.themed-button-like {
@@ -486,7 +548,7 @@ form.crossable.htmx-request input {
}
.custom-svg-marker svg {
- filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.3));
+ filter: drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.3));
transition: transform 0.2s;
}
@@ -584,7 +646,8 @@ form.crossable.htmx-request input {
}
@container main (width >= 76rem) {
- .grid-item, .grid-sizer {
+ .grid-item,
+ .grid-sizer {
width: 18rem;
}
@@ -608,12 +671,20 @@ form.crossable.htmx-request input {
}
.goverlay {
- background: color-mix(in srgb, var(--color-background-light) 50%, transparent) !important;
+ background: color-mix(
+ in srgb,
+ var(--color-background-light) 50%,
+ transparent
+ ) !important;
backdrop-filter: blur(1rem);
}
.glightbox-clean .gslide-description {
- background: color-mix(in srgb, var(--color-background-dark) 50%, transparent) !important;
+ background: color-mix(
+ in srgb,
+ var(--color-background-dark) 50%,
+ transparent
+ ) !important;
backdrop-filter: blur(1rem);
}
diff --git a/views/layouts/general-mail.html b/views/layouts/general-mail.html
index 00bd964..452982a 100644
--- a/views/layouts/general-mail.html
+++ b/views/layouts/general-mail.html
@@ -1,99 +1,119 @@
-<!DOCTYPE html>
+<!doctype html>
<html lang="en">
- <meta charset="UTF-8">
+ <meta charset="UTF-8" />
<head>
<style>
-@font-face {
- font-family: 'Gentium Plus';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: local('Gentium Plus'),
- url(https://fonts.gstatic.com/s/gentiumplus/v2/Iurd6Ytw-oSPaZ00r2bNe8VZj5FNym499r7e.woff2) format('woff2');
- unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
-}
-@font-face {
- font-family: 'Gentium Plus';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: local('Gentium Plus'),
- url(https://fonts.gstatic.com/s/gentiumplus/v2/Iurd6Ytw-oSPaZ00r2bNe8VZi5FNym499g.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;
-}
-html {
- font-family: 'Gentium Plus', sans-serif;
- --bg-paper: url(data:image/webp;base64,UklGRi4BAABXRUJQVlA4ICIBAABQCQCdASqAAIAAPm02lUmkIqIkIImIgA2JaW3rMFmGDAOKLAoJtP591ByFR74Tun2DP+Lygpfsz5FH7+cKgxi2Islfd03i5AJdXUfU0SDc8SXYAAD++hlzeYAJWQEF8ZdkJdlcA+5btZg0FMGaso8SHoBph0qAOQysElJ5C5SJFNoKIV1J550UyL7Y9Gh0/c/uzLnBmr8lvz7k1WeA81H4S5ViU/wBqa7aM1m8D9rDtrk8cSTqx+xWF6MS8UfZGSsks6CT/igtgv6syFN9uNA3LjnjiCJKYOvFcMkX+jB2578GWVo3nyLaM1QNPYCxn3TiUbnf7U0MdD2NsBpg1+Jd1wu6yW/U+goN+IQS6Nt4RMWltVcZHlxrneKJi0SobO0AAA==);
-}
-body {
- background-color: #e3e6ff;
-}
-a {
- text-decoration: underline;
- text-shadow: none !important;
-}
-.header {
- width: 100%;
- padding: 8px 8px 8px 32px;
- font-size: 48px;
- color: white;
- text-shadow: -2px 0 #343b58, 0 2px #343b58, 2px 0 #343b58, 0 -2px #343b58;
- background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='40' height='80'><path fill='#e5e8ff' d='M0 0h40v80H0z'/><g fill='none' stroke-linecap='square' stroke-width='4.5'><path stroke='#8d4453' d='m-10 5 20 10L30 5l20 10'/><path stroke='#375e0d' d='m-10 25 20 10 20-10 20 10'/><path stroke='#006a85' d='m-10 45 20 10 20-10 20 10'/><path stroke='#5a3e8e' d='m-10 65 20 10 20-10 20 10'/></g></svg>");
- background-color: #e3e6ff;
- background-size: contain;
- background-repeat: repeat-x;
-}
-.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(--bg-paper);
- background-blend-mode: multiply;
- background-repeat: repeat;
- background-size: 128px;
-}
-.footer {
- margin: 0;
- padding: 1px 8px 1px 8px;
- font-size: 16px;
- color: white;
-}
-.darkened {
- background-color: #9aa5f4;
- background-image: var(--bg-paper);
- background-blend-mode: multiply;
- background-repeat: repeat;
- background-size: 128px;
-}
-.outlier {
- padding-inline: 4px;
- background-color: #6c6e75;
- background-image: var(--bg-paper);
- background-blend-mode: multiply;
- background-repeat: repeat;
- background-size: 128px;
-}
-table,th,td {
- border: 1px solid black;
-}
-td {
- padding: 5px;
-}
-img {
- width: 200px;
-}
+ @font-face {
+ font-family: "Gentium Plus";
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src:
+ local("Gentium Plus"),
+ url(https://fonts.gstatic.com/s/gentiumplus/v2/Iurd6Ytw-oSPaZ00r2bNe8VZj5FNym499r7e.woff2)
+ format("woff2");
+ unicode-range:
+ U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+ }
+ @font-face {
+ font-family: "Gentium Plus";
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src:
+ local("Gentium Plus"),
+ url(https://fonts.gstatic.com/s/gentiumplus/v2/Iurd6Ytw-oSPaZ00r2bNe8VZi5FNym499g.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;
+ }
+ html {
+ font-family: "Gentium Plus", sans-serif;
+ --bg-paper: url(data:image/webp;base64,UklGRi4BAABXRUJQVlA4ICIBAABQCQCdASqAAIAAPm02lUmkIqIkIImIgA2JaW3rMFmGDAOKLAoJtP591ByFR74Tun2DP+Lygpfsz5FH7+cKgxi2Islfd03i5AJdXUfU0SDc8SXYAAD++hlzeYAJWQEF8ZdkJdlcA+5btZg0FMGaso8SHoBph0qAOQysElJ5C5SJFNoKIV1J550UyL7Y9Gh0/c/uzLnBmr8lvz7k1WeA81H4S5ViU/wBqa7aM1m8D9rDtrk8cSTqx+xWF6MS8UfZGSsks6CT/igtgv6syFN9uNA3LjnjiCJKYOvFcMkX+jB2578GWVo3nyLaM1QNPYCxn3TiUbnf7U0MdD2NsBpg1+Jd1wu6yW/U+goN+IQS6Nt4RMWltVcZHlxrneKJi0SobO0AAA==);
+ }
+ body {
+ background-color: #e3e6ff;
+ }
+ a {
+ text-decoration: underline;
+ text-shadow: none !important;
+ }
+ .header {
+ width: 100%;
+ padding: 8px 8px 8px 32px;
+ font-size: 48px;
+ color: white;
+ text-shadow:
+ -2px 0 #343b58,
+ 0 2px #343b58,
+ 2px 0 #343b58,
+ 0 -2px #343b58;
+ background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='40' height='80'><path fill='#e5e8ff' d='M0 0h40v80H0z'/><g fill='none' stroke-linecap='square' stroke-width='4.5'><path stroke='#8d4453' d='m-10 5 20 10L30 5l20 10'/><path stroke='#375e0d' d='m-10 25 20 10 20-10 20 10'/><path stroke='#006a85' d='m-10 45 20 10 20-10 20 10'/><path stroke='#5a3e8e' d='m-10 65 20 10 20-10 20 10'/></g></svg>");
+ background-color: #e3e6ff;
+ background-size: contain;
+ background-repeat: repeat-x;
+ }
+ .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(--bg-paper);
+ background-blend-mode: multiply;
+ background-repeat: repeat;
+ background-size: 128px;
+ }
+ .footer {
+ margin: 0;
+ padding: 1px 8px 1px 8px;
+ font-size: 16px;
+ color: white;
+ }
+ .darkened {
+ background-color: #9aa5f4;
+ background-image: var(--bg-paper);
+ background-blend-mode: multiply;
+ background-repeat: repeat;
+ background-size: 128px;
+ }
+ .outlier {
+ padding-inline: 4px;
+ background-color: #6c6e75;
+ background-image: var(--bg-paper);
+ background-blend-mode: multiply;
+ background-repeat: repeat;
+ background-size: 128px;
+ }
+ 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="header" class="header">saya.uz</div>
- <div id="content" class="content" style="background-color: #e3e6ff !important;">
+ <div
+ id="content"
+ class="content"
+ style="background-color: #e3e6ff !important"
+ >
{{ block "body" . }}{{ end }}
</div>
diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html
index 93567d3..0bd3454 100644
--- a/views/layouts/general-page.html
+++ b/views/layouts/general-page.html
@@ -1,469 +1,725 @@
-<!DOCTYPE html>
+<!doctype html>
<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- {{- range .Meta }}
- <meta {{with .Name}}name="{{.}}" {{end}}{{with .Property}}property="{{.}}" {{end}}{{with .Content}}content="{{.}}"{{end}}>
- {{- if eq .Property "og:description" }}
- <meta name="description" content="{{.Content}}">
- {{- end }}
- {{- if eq .Property "og:url" }}
- <link rel="canonical" href="{{.Content}}">
- {{- end }}
- {{- end }}
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/fontawesome.min.css">
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/brands.min.css">
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/solid.min.css">
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.css">
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/glightbox/3.3.0/css/glightbox.min.css">
- <link href="/output.css" rel="stylesheet">
- {{- with .LinkedData }}
- <script type="application/ld+json">
- {{ . }}
- </script>
- {{- end }}
-</head>
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ {{- range .Meta }}
+ <meta
+ {{with
+ .Name}}name="{{.}}"
+ {{end}}{{with
+ .Property}}property="{{.}}"
+ {{end}}{{with
+ .Content}}content="{{.}}"
+ {{end}}
+ />
+ {{- if eq .Property "og:description" }}
+ <meta name="description" content="{{.Content}}" />
+ {{- end }} {{- if eq .Property "og:url" }}
+ <link rel="canonical" href="{{.Content}}" />
+ {{- end }} {{- end }}
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/fonts/fonts.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/fontawesome.min.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/brands.min.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/solid.min.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/glightbox/3.3.0/css/glightbox.min.css"
+ />
+ <link rel="stylesheet" href="/output.css" />
+ <link
+ rel="icon"
+ href="/favicon-light.svg"
+ type="image/svg+xml"
+ media="(prefers-color-scheme: light)"
+ />
+ <link
+ rel="icon"
+ href="/favicon-dark.svg"
+ type="image/svg+xml"
+ media="(prefers-color-scheme: dark)"
+ />
+ {{- with .LinkedData }}
+ <script type="application/ld+json">
+ {{ . }}
+ </script>
+ {{- end }}
+ </head>
-<body data-theme="ram" class="font-m-plus text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh flex flex-row">
+ <body
+ data-theme="ram"
+ class="font-m-plus text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh flex flex-row"
+ >
+ <script>
+ let map = null;
+ </script>
- <script>
- let map = null;
- </script>
+ <script>
+ function changeUrl(path, toAppend = false) {
+ const urlParams = new URLSearchParams(window.location.search);
+ newPathname = location.pathname.endsWith("/")
+ ? location.pathname.slice(0, -1)
+ : location.pathname;
+ while (path.slice(-3) == "/.." && toAppend) {
+ newPathname = newPathname.replace(/\/[^\/]+$/, "");
+ path = path.slice(0, -3);
+ }
+ const newPath = toAppend ? `${newPathname}${path}` : `${path}`;
- <script>
- function changeUrl(path, toAppend = false) {
- const urlParams = new URLSearchParams(window.location.search);
- newPathname = location.pathname.endsWith('/') ? location.pathname.slice(0, -1) : location.pathname;
- while (path.slice(-3) == '/..' && toAppend) {
- newPathname = newPathname.replace(/\/[^\/]+$/, '');
- path = path.slice(0, -3);
+ history.pushState(
+ {},
+ "",
+ `${newPath}${urlParams.entries.length == 0 ? "" : "?" + urlParams.toString()}`,
+ );
+ window.dispatchEvent(new Event("popstate"));
+ return false;
}
- const newPath = toAppend ? `${newPathname}${path}` : `${path}`;
-
- history.pushState({}, '', `${newPath}${urlParams.entries.length == 0 ? '' : '?' + urlParams.toString()}`);
- window.dispatchEvent(new Event('popstate'));
- return false;
- };
- function convertRemToPixels(rem) {
- return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
- }
- </script>
+ function convertRemToPixels(rem) {
+ return (
+ rem *
+ parseFloat(
+ getComputedStyle(document.documentElement).fontSize,
+ )
+ );
+ }
+ </script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/glightbox/3.3.0/js/glightbox.min.js"></script>
+ <script src="{{ .StaticStorageBaseUrl }}/libs/glightbox/3.3.0/js/glightbox.min.js"></script>
- <script>
- function sfc32(a, b, c, d) {
- return function() {
- a |= 0; b |= 0; c |= 0; d |= 0;
- let t = (a + b | 0) + d | 0;
- d = d + 1 | 0;
- a = b ^ b >>> 9;
- b = c + (c << 3) | 0;
- c = (c << 21 | c >>> 11);
- c = c + t | 0;
- return (t >>> 0) / 4294967296;
+ <script>
+ function sfc32(a, b, c, d) {
+ return function () {
+ a |= 0;
+ b |= 0;
+ c |= 0;
+ d |= 0;
+ let t = (((a + b) | 0) + d) | 0;
+ d = (d + 1) | 0;
+ a = b ^ (b >>> 9);
+ b = (c + (c << 3)) | 0;
+ c = (c << 21) | (c >>> 11);
+ c = (c + t) | 0;
+ return (t >>> 0) / 4294967296;
+ };
}
- }
- function cyrb128(str) {
- let h1 = 1779033703, h2 = 3144134277,
- h3 = 1013904242, h4 = 2773480762;
- for (let i = 0, k; i < str.length; i++) {
- k = str.charCodeAt(i);
- h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
- h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
- h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
- h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
+ function cyrb128(str) {
+ let h1 = 1779033703,
+ h2 = 3144134277,
+ h3 = 1013904242,
+ h4 = 2773480762;
+ for (let i = 0, k; i < str.length; i++) {
+ k = str.charCodeAt(i);
+ h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
+ h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
+ h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
+ h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
+ }
+ h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
+ h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
+ h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
+ h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
+ ((h1 ^= h2 ^ h3 ^ h4), (h2 ^= h1), (h3 ^= h1), (h4 ^= h1));
+ return [h1 >>> 0, h2 >>> 0, h3 >>> 0, h4 >>> 0];
}
- h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
- h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
- h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
- h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
- h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1;
- return [h1>>>0, h2>>>0, h3>>>0, h4>>>0];
- }
- var seed = cyrb128("{{ .Title }}");
- var rand = sfc32(seed[0], seed[1], seed[2], seed[3]);
+ var seed = cyrb128("{{ .Title }}");
+ var rand = sfc32(seed[0], seed[1], seed[2], seed[3]);
- function calculateVmin(percent) {
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
+ function calculateVmin(percent) {
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
- const smallerDimension = Math.min(viewportWidth, viewportHeight);
+ const smallerDimension = Math.min(
+ viewportWidth,
+ viewportHeight,
+ );
- const vminValue = (smallerDimension / 100) * percent;
+ const vminValue = (smallerDimension / 100) * percent;
- return vminValue;
- }
+ return vminValue;
+ }
- function switchThemeDecor(theme) {
- document.querySelectorAll('.theme-decor').forEach((e) => {
- e.remove();
- });
+ function switchThemeDecor(theme) {
+ document.querySelectorAll(".theme-decor").forEach((e) => {
+ e.remove();
+ });
- if (theme == "" || typeof theme == "undefined") {
- theme = document.body.getAttribute('data-theme');
- }
+ if (theme == "" || typeof theme == "undefined") {
+ theme = document.body.getAttribute("data-theme");
+ }
- vmin = calculateVmin(1);
- var clWidth = document.documentElement.clientWidth;
- var clHeight = document.documentElement.clientHeight;
+ vmin = calculateVmin(1);
+ var clWidth = document.documentElement.clientWidth;
+ var clHeight = document.documentElement.clientHeight;
- switch (theme) {
- case "olive": break;
- case "lettuce": break;
- case "night":
- for (i = 0; i < 25; i++) {
- let star = document.createElement("div");
- star.append("*");
- star.classList.add("night-star", "theme-decor", "z-0", "absolute");
- const x = Math.floor(rand() * clWidth);
- const y = Math.floor(rand() * clHeight * 0.8);
- const rot = rand() * 90;
- const animDuration = (rand() * 10) + 3;
- const sz = Math.floor(rand() * 8) + 12;
- star.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`;
- star.style.animationDuration = `${animDuration}s`;
- star.style.fontSize = `${sz}px`;
- document.body.append(star);
- }
- break;
- case "ram":
- const frameCenters = new Array(4);
- for (i = 1; i <= 4; i++) {
- let frame = document.createElement("img");
- frame.src = `{{ .StaticStorageBaseUrl }}/themes/ram/ram-frame${i}.webp`;
- frame.classList.add("ram-frame", "theme-decor", "z-0", "absolute");
- let x, y;
- outerLoop:
- while (true) {
- x = Math.floor(rand() * clWidth - 20*vmin);
- y = Math.floor(rand() * clHeight - 20*vmin);
- for (j = 0; j < i-1; j++) {
- const xd = frameCenters[j].x - x;
- const yd = frameCenters[j].y - y;
- if (Math.sqrt(xd*xd + yd*yd) < 30*vmin) {
- continue outerLoop;
+ switch (theme) {
+ case "olive":
+ break;
+ case "lettuce":
+ break;
+ case "night":
+ for (i = 0; i < 25; i++) {
+ let star = document.createElement("div");
+ star.append("*");
+ star.classList.add(
+ "night-star",
+ "theme-decor",
+ "z-0",
+ "absolute",
+ );
+ const x = Math.floor(rand() * clWidth);
+ const y = Math.floor(rand() * clHeight * 0.8);
+ const rot = rand() * 90;
+ const animDuration = rand() * 10 + 3;
+ const sz = Math.floor(rand() * 8) + 12;
+ star.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`;
+ star.style.animationDuration = `${animDuration}s`;
+ star.style.fontSize = `${sz}px`;
+ document.body.append(star);
+ }
+ break;
+ case "ram":
+ const frameCenters = new Array(4);
+ for (i = 1; i <= 4; i++) {
+ let frame = document.createElement("img");
+ frame.src = `{{ .StaticStorageBaseUrl }}/themes/ram/ram-frame${i}.webp`;
+ frame.classList.add(
+ "ram-frame",
+ "theme-decor",
+ "z-0",
+ "absolute",
+ );
+ let x, y;
+ outerLoop: while (true) {
+ x = Math.floor(rand() * clWidth - 20 * vmin);
+ y = Math.floor(rand() * clHeight - 20 * vmin);
+ for (j = 0; j < i - 1; j++) {
+ const xd = frameCenters[j].x - x;
+ const yd = frameCenters[j].y - y;
+ if (
+ Math.sqrt(xd * xd + yd * yd) <
+ 30 * vmin
+ ) {
+ continue outerLoop;
+ }
}
+ break;
}
- break;
+ frameCenters[i - 1] = { x: x, y: y };
+ const rot = rand() * 90 - 45;
+ frame.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`;
+ document.body.append(frame);
}
- frameCenters[i-1] = {x: x, y: y};
- const rot = rand() * 90 - 45;
- frame.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`;
- document.body.append(frame);
- }
- break;
- case "lilac": break;
+ break;
+ case "lilac":
+ break;
+ }
}
- }
- function switchTheme(theme) {
- if (theme == "" || typeof theme == "undefined") {
- const currentTheme = document.body.getAttribute('data-theme');
- switch (currentTheme) {
- case "olive": theme = "lettuce"; break;
- case "lettuce": theme = "night"; break;
- case "night": theme = "ram"; break;
- case "ram": theme = "lilac"; break;
- case "lilac": theme = "olive"; break;
+ function switchTheme(theme) {
+ if (theme == "" || typeof theme == "undefined") {
+ const currentTheme =
+ document.body.getAttribute("data-theme");
+ switch (currentTheme) {
+ case "olive":
+ theme = "lettuce";
+ break;
+ case "lettuce":
+ theme = "night";
+ break;
+ case "night":
+ theme = "ram";
+ break;
+ case "ram":
+ theme = "lilac";
+ break;
+ case "lilac":
+ theme = "olive";
+ break;
+ }
}
+ localStorage.setItem("theme", theme);
+ document.body.setAttribute("data-theme", theme);
+ switchThemeDecor(theme);
}
- localStorage.setItem('theme', theme);
- document.body.setAttribute('data-theme', theme);
- switchThemeDecor(theme);
- }
- const savedTheme = localStorage.getItem('theme') || 'lilac';
- switchTheme(savedTheme);
- </script>
+ const savedTheme = localStorage.getItem("theme") || "lilac";
+ switchTheme(savedTheme);
+ </script>
- <div id="general-page-top-embeds" class="flex"
- hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
- </div>
- <div id="general-page-top-embeds-trigger" hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-top-embeds" hx-swap="innerHTML"></div>
+ <div
+ id="general-page-top-embeds"
+ class="flex"
+ hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
+ hx-trigger="load"
+ hx-target="this"
+ hx-swap="innerHTML"
+ ></div>
+ <div
+ id="general-page-top-embeds-trigger"
+ hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
+ hx-trigger="popstate from:window"
+ hx-target="#general-page-top-embeds"
+ hx-swap="innerHTML"
+ ></div>
- <script>
- const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
- </script>
+ <script>
+ const clientTimeZone =
+ Intl.DateTimeFormat().resolvedOptions().timeZone;
+ </script>
- <div class="grow bg-split-left"></div>
+ <div class="grow bg-split-left"></div>
- <div class="flex flex-col sm:flex-row">
- <div id="sidebar" class="bg-sidebar flex
- flex-row sticky w-dvw h-fit xs:w-[90dvw]
- sm:left-0 sm:flex-col sm:fixed sm:w-fit
- z-20 shadow-[0_0_1rem_black]">
- <nav aria-label="Main navigation" class="text-[3rem] flex flex-row sm:flex-col gap-0 relative z-20">
- <a onclick="return changeUrl('/..', true);" href="../" id="sidebar-back-button" class="themed-button themed-button-sidebar w-fit h-fit hidden" draggable="false">
- <i class="fas fa-circle-left flex faw-18 h-18 content-center text-center text-sidebar"></i>
- </a>
- <a onclick="return changeUrl('/{{ .Lang }}');" href="/{{ .Lang }}" id="sidebar-home-button" class="themed-button themed-button-sidebar w-fit h-fit hidden" draggable="false">
- <i class="fas fa-home flex faw-18 h-18 content-center text-center text-sidebar"></i>
- </a>
- <a onclick="return changeUrl('/{{ .Lang }}/blog');" href="/{{ .Lang }}/blog" id="sidebar-blog-button" class="themed-button themed-button-sidebar w-fit h-fit hidden" draggable="false">
- <i class="fas fa-search flex faw-18 h-18 content-center text-center text-sidebar"></i>
- </a>
- <a href="/{{ .Lang }}/map" id="sidebar-map-button" class="themed-button themed-button-sidebar w-fit h-fit hidden" draggable="false">
- <i class="fas fa-map flex faw-18 h-18 content-center text-center text-sidebar"></i>
- </a>
- <div id="toolbar-theme-button" onclick="toggleThemeWheel();" class="themed-button themed-button-sidebar w-fit h-fit" draggable="false">
- <i id="toolbar-theme-button-content" class="fas fa-swatchbook flex faw-18 h-18 content-center text-center text-sidebar"></i>
+ <div class="flex flex-col sm:flex-row">
+ <div
+ id="sidebar"
+ class="bg-sidebar flex flex-row sticky w-dvw h-fit xs:w-[90dvw] sm:left-0 sm:flex-col sm:fixed sm:w-fit z-20 shadow-[0_0_1rem_black]"
+ >
+ <nav
+ aria-label="Main navigation"
+ class="text-[3rem] flex flex-row sm:flex-col gap-0 relative z-20"
+ >
+ <a
+ onclick="return changeUrl('/..', true);"
+ href="../"
+ id="sidebar-back-button"
+ class="themed-button themed-button-sidebar w-fit h-fit hidden"
+ draggable="false"
+ >
+ <i
+ class="fas fa-circle-left flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </a>
+ <a
+ onclick="return changeUrl('/{{ .Lang }}');"
+ href="/{{ .Lang }}"
+ id="sidebar-home-button"
+ class="themed-button themed-button-sidebar w-fit h-fit hidden"
+ draggable="false"
+ >
+ <i
+ class="fas fa-home flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </a>
+ <a
+ onclick="return changeUrl('/{{ .Lang }}/blog');"
+ href="/{{ .Lang }}/blog"
+ id="sidebar-blog-button"
+ class="themed-button themed-button-sidebar w-fit h-fit hidden"
+ draggable="false"
+ >
+ <i
+ class="fas fa-search flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </a>
+ <a
+ href="/{{ .Lang }}/map"
+ id="sidebar-map-button"
+ class="themed-button themed-button-sidebar w-fit h-fit hidden"
+ draggable="false"
+ >
+ <i
+ class="fas fa-map flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </a>
+ <div
+ id="toolbar-theme-button"
+ onclick="toggleThemeWheel()"
+ class="themed-button themed-button-sidebar w-fit h-fit"
+ draggable="false"
+ >
+ <i
+ id="toolbar-theme-button-content"
+ class="fas fa-swatchbook flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </div>
+ <a
+ onclick="return changeUrl('/{{ .Lang }}/user');"
+ href="/{{ .Lang }}/user"
+ id="sidebar-user-button"
+ class="themed-button themed-button-sidebar w-fit h-fit hidden"
+ draggable="false"
+ >
+ <i
+ class="fas fa-user flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </a>
+ </nav>
+ <div class="theme-wheel" id="theme-wheel">
+ <div class="theme-icon" data-theme="olive"></div>
+ <div class="theme-icon" data-theme="lettuce"></div>
+ <div class="theme-icon" data-theme="night"></div>
+ <div class="theme-icon" data-theme="ram"></div>
+ <div class="theme-icon" data-theme="lilac"></div>
+ <div class="theme-pin"></div>
</div>
- <a onclick="return changeUrl('/{{ .Lang }}/user');" href="/{{ .Lang }}/user" id="sidebar-user-button" class="themed-button themed-button-sidebar w-fit h-fit hidden" draggable="false">
- <i class="fas fa-user flex faw-18 h-18 content-center text-center text-sidebar"></i>
- </a>
- </nav>
- <div class="theme-wheel" id="theme-wheel">
- <div class="theme-icon" data-theme="olive"></div>
- <div class="theme-icon" data-theme="lettuce"></div>
- <div class="theme-icon" data-theme="night"></div>
- <div class="theme-icon" data-theme="ram"></div>
- <div class="theme-icon" data-theme="lilac"></div>
- <div class="theme-pin"></div>
- </div>
- <script>
- sidebarHomeButton = document.getElementById("sidebar-home-button");
- sidebarMapButton = document.getElementById("sidebar-map-button");
- sidebarBlogButton = document.getElementById("sidebar-blog-button");
- sidebarBackButton = document.getElementById("sidebar-back-button");
- sidebarUserButton = document.getElementById("sidebar-user-button");
+ <script>
+ sidebarHomeButton = document.getElementById(
+ "sidebar-home-button",
+ );
+ 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 == "/") {
- sidebarHomeButton.classList.add('hidden');
- 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.setAttribute('onclick', `return changeUrl('/${lang}', false);`);
- sidebarMapButton.setAttribute('href', `/${lang}/map`);
- sidebarBlogButton.setAttribute('href', `/${lang}/blog`);
- sidebarBlogButton.setAttribute('onclick', `return changeUrl('/${lang}/blog', false);`);
- sidebarUserButton.setAttribute('href', `/${lang}/user`);
- sidebarUserButton.setAttribute('onclick', `return changeUrl('/${lang}/user', false);`);
+ function updateSidebar() {
+ if (
+ location.pathname == "" ||
+ location.pathname == "/"
+ ) {
+ sidebarHomeButton.classList.add("hidden");
+ 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.setAttribute(
+ "onclick",
+ `return changeUrl('/${lang}', false);`,
+ );
+ sidebarMapButton.setAttribute(
+ "href",
+ `/${lang}/map`,
+ );
+ sidebarBlogButton.setAttribute(
+ "href",
+ `/${lang}/blog`,
+ );
+ sidebarBlogButton.setAttribute(
+ "onclick",
+ `return changeUrl('/${lang}/blog', false);`,
+ );
+ sidebarUserButton.setAttribute(
+ "href",
+ `/${lang}/user`,
+ );
+ sidebarUserButton.setAttribute(
+ "onclick",
+ `return changeUrl('/${lang}/user', false);`,
+ );
+ }
}
- }
- window.addEventListener("popstate", updateSidebar);
- window.addEventListener("load", updateSidebar, {once: true});
- </script>
- </div>
+ window.addEventListener("popstate", updateSidebar);
+ window.addEventListener("load", updateSidebar, {
+ once: true,
+ });
+ </script>
+ </div>
- <div class="bg-paper bg-background-dark @container/main flex z-10 w-dvw xs:w-[90dvw] sm:w-[85dvw] md:w-[80dvw] lg:w-[75dvw] xl:w-[70dvw] 2xl:w-[60dvw] 3xl:w-[50dvw] grow-0 h-[calc(100dvh-5rem)] sm:h-dvh shadow-[0_0_1rem_black]">
- <div class="bg-paper bg-background-dark border-r-2 border-dashed border-main-hard"></div>
+ <div
+ class="bg-paper bg-background-dark @container/main flex z-10 w-dvw xs:w-[90dvw] sm:w-[85dvw] md:w-[80dvw] lg:w-[75dvw] xl:w-[70dvw] 2xl:w-[60dvw] 3xl:w-[50dvw] grow-0 h-[calc(100dvh-5rem)] sm:h-dvh shadow-[0_0_1rem_black]"
+ >
+ <div
+ class="bg-paper bg-background-dark border-r-2 border-dashed border-main-hard"
+ ></div>
- <div class="max-h-full max-w-full flex flex-col grow bg-paper bg-background-dark relative pt-4 ml-8 pr-4">
- <header id="general-page-header" class="max-xs:hidden [@media(max-height:32rem)]:hidden flex flex-col basis-8"
- hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
- </header>
- <div id="general-page-header-trigger" hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-header" hx-swap="innerHTML"></div>
+ <div
+ class="max-h-full max-w-full flex flex-col grow bg-paper bg-background-dark relative pt-4 ml-8 pr-4"
+ >
+ <header
+ id="general-page-header"
+ class="max-xs:hidden [@media(max-height:32rem)]:hidden flex flex-col basis-8"
+ hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
+ hx-trigger="load"
+ hx-target="this"
+ hx-swap="innerHTML"
+ ></header>
+ <div
+ id="general-page-header-trigger"
+ hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
+ hx-trigger="popstate from:window"
+ hx-target="#general-page-header"
+ hx-swap="innerHTML"
+ ></div>
- <hr class="max-xs:hidden [@media(max-height:32rem)]:hidden border-t-4 border-dotted border-main-hard mb-2">
+ <hr
+ class="max-xs:hidden [@media(max-height:32rem)]:hidden border-t-4 border-dotted border-main-hard mb-2"
+ />
- <main id="general-page-body" class="relative flex flex-col grow overflow-y-auto"
- hx-get="/api/v1/general-page/body{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
- </main>
- <div id="general-page-body-trigger" hx-get="/api/v1/general-page/body{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-body" hx-swap="innerHTML"></div>
+ <main
+ id="general-page-body"
+ class="relative flex flex-col grow overflow-y-auto"
+ hx-get="/api/v1/general-page/body{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
+ hx-trigger="load"
+ hx-target="this"
+ hx-swap="innerHTML"
+ ></main>
+ <div
+ id="general-page-body-trigger"
+ hx-get="/api/v1/general-page/body{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
+ hx-trigger="popstate from:window"
+ hx-target="#general-page-body"
+ hx-swap="innerHTML"
+ ></div>
- <footer id="general-page-footer" class="flex flex-row basis-4 my-1"
- hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
- </footer>
- <div id="general-page-footer-trigger" hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-footer" hx-swap="innerHTML"></div>
+ <footer
+ id="general-page-footer"
+ class="flex flex-row basis-4 my-1"
+ hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
+ hx-trigger="load"
+ hx-target="this"
+ hx-swap="innerHTML"
+ ></footer>
+ <div
+ id="general-page-footer-trigger"
+ hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
+ hx-trigger="popstate from:window"
+ hx-target="#general-page-footer"
+ hx-swap="innerHTML"
+ ></div>
- <hr class="border-t-4 border-dotted border-main-hard mb-2">
- </div>
+ <hr
+ class="border-t-4 border-dotted border-main-hard mb-2"
+ />
+ </div>
- <div class="pr-[0.5dvw] bg-paper bg-background-dark shadow-2xl border-l-2 border-dotted border-main-hard"></div>
+ <div
+ class="pr-[0.5dvw] bg-paper bg-background-dark shadow-2xl border-l-2 border-dotted border-main-hard"
+ ></div>
+ </div>
</div>
- </div>
- <div class="grow bg-split-right"></div>
+ <div class="grow bg-split-right"></div>
- <script src="{{ .StaticStorageBaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/imagesloaded/5.0.0/imagesloaded.pkgd.min.js"></script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/packery/3.0.0/dist/packery.pkgd.min.js"></script>
+ <script src="{{ .StaticStorageBaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script>
+ <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script>
+ <script src="{{ .StaticStorageBaseUrl }}/libs/imagesloaded/5.0.0/imagesloaded.pkgd.min.js"></script>
+ <script src="{{ .StaticStorageBaseUrl }}/libs/packery/3.0.0/dist/packery.pkgd.min.js"></script>
- <script>
- // Enable fitWidth-like attribute for Packery. Source: https://codepen.io/desandro/pen/kVmLYz
- var __resetLayout = Packery.prototype._resetLayout;
- Packery.prototype._resetLayout = function() {
- __resetLayout.call( this );
- var parentSize = getSize( this.element.parentNode );
- var colW = this.columnWidth + this.gutter;
- this.fitWidth = Math.floor( ( parentSize.innerWidth + this.gutter ) / colW ) * colW;
- this.packer.width = this.fitWidth;
- this.packer.height = Number.POSITIVE_INFINITY;
- this.packer.reset();
- };
+ <script>
+ // Enable fitWidth-like attribute for Packery. Source: https://codepen.io/desandro/pen/kVmLYz
+ var __resetLayout = Packery.prototype._resetLayout;
+ Packery.prototype._resetLayout = function () {
+ __resetLayout.call(this);
+ var parentSize = getSize(this.element.parentNode);
+ var colW = this.columnWidth + this.gutter;
+ this.fitWidth =
+ Math.floor((parentSize.innerWidth + this.gutter) / colW) *
+ colW;
+ this.packer.width = this.fitWidth;
+ this.packer.height = Number.POSITIVE_INFINITY;
+ this.packer.reset();
+ };
- Packery.prototype._getContainerSize = function() {
- var emptyWidth = 0;
- for ( var i=0, len = this.packer.spaces.length; i < len; i++ ) {
- var space = this.packer.spaces[i];
- if ( space.y === 0 && space.height === Number.POSITIVE_INFINITY ) {
- emptyWidth += space.width;
+ Packery.prototype._getContainerSize = function () {
+ var emptyWidth = 0;
+ for (var i = 0, len = this.packer.spaces.length; i < len; i++) {
+ var space = this.packer.spaces[i];
+ if (
+ space.y === 0 &&
+ space.height === Number.POSITIVE_INFINITY
+ ) {
+ emptyWidth += space.width;
+ }
}
- }
- return {
- width: this.fitWidth - this.gutter - emptyWidth,
- height: this.maxY - this.gutter
+ return {
+ width: this.fitWidth - this.gutter - emptyWidth,
+ height: this.maxY - this.gutter,
+ };
};
- };
- Packery.prototype.needsResizeLayout = function() {
- return true;
- };
- </script>
+ Packery.prototype.needsResizeLayout = function () {
+ return true;
+ };
+ </script>
- <div id="general-page-bottom-embeds" class="flex"
- hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
- </div>
- <div id="general-page-bottom-embeds-trigger" hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-bottom-embeds" hx-swap="innerHTML"></div>
+ <div
+ id="general-page-bottom-embeds"
+ class="flex"
+ hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
+ hx-trigger="load"
+ hx-target="this"
+ hx-swap="innerHTML"
+ ></div>
+ <div
+ id="general-page-bottom-embeds-trigger"
+ hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
+ hx-trigger="popstate from:window"
+ hx-target="#general-page-bottom-embeds"
+ hx-swap="innerHTML"
+ ></div>
- <script>
- const themeWheel = document.getElementById('theme-wheel');
- const themeButton = document.getElementById('toolbar-theme-button');
- const themeIcons = document.querySelectorAll('.theme-icon');
- const themeWheelRadius = convertRemToPixels(6);
+ <script>
+ const themeWheel = document.getElementById("theme-wheel");
+ const themeButton = document.getElementById("toolbar-theme-button");
+ const themeIcons = document.querySelectorAll(".theme-icon");
+ const themeWheelRadius = convertRemToPixels(6);
- themeIcons.forEach(icon => {
- if (icon.getAttribute('data-theme') == savedTheme) {
- icon.classList.add('active');
- }
- });
+ themeIcons.forEach((icon) => {
+ if (icon.getAttribute("data-theme") == savedTheme) {
+ icon.classList.add("active");
+ }
+ });
- function calculateVmax(percent) {
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
+ function calculateVmax(percent) {
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
- const largerDimension = Math.max(viewportWidth, viewportHeight);
+ const largerDimension = Math.max(viewportWidth, viewportHeight);
- const vmaxValue = (largerDimension / 100) * percent;
+ const vmaxValue = (largerDimension / 100) * percent;
- return vmaxValue;
- }
+ return vmaxValue;
+ }
- function positionThemeIcons() {
- const sliceAngle = 360 / themeIcons.length;
+ function positionThemeIcons() {
+ const sliceAngle = 360 / themeIcons.length;
- themeIcons.forEach((icon, index) => {
- const startAngle = index * sliceAngle;
- const endAngle = (index + 1) * sliceAngle;
+ themeIcons.forEach((icon, index) => {
+ const startAngle = index * sliceAngle;
+ const endAngle = (index + 1) * sliceAngle;
- const clipPath = createSliceClipPath(startAngle, endAngle, themeWheelRadius);
- icon.style.clipPath = clipPath;
- });
- }
+ const clipPath = createSliceClipPath(
+ startAngle,
+ endAngle,
+ themeWheelRadius,
+ );
+ icon.style.clipPath = clipPath;
+ });
+ }
+
+ function createSliceClipPath(startAngle, endAngle, radius) {
+ const centerX = radius;
+ const centerY = radius;
+
+ let points = `${centerX}px ${centerY}px`;
- function createSliceClipPath(startAngle, endAngle, radius) {
- const centerX = radius;
- const centerY = radius;
+ const segments = Math.max(
+ 2,
+ Math.ceil(Math.abs(endAngle - startAngle) / 5),
+ );
+ const angleStep = (endAngle - startAngle) / segments;
- let points = `${centerX}px ${centerY}px`;
+ for (let i = 0; i <= segments; i++) {
+ const angle =
+ ((startAngle + angleStep * i - 90) * Math.PI) / 180;
+ const x = centerX + Math.cos(angle) * radius;
+ const y = centerY + Math.sin(angle) * radius;
+ points += `, ${x}px ${y}px`;
+ }
- const segments = Math.max(2, Math.ceil(Math.abs(endAngle - startAngle) / 5));
- const angleStep = (endAngle - startAngle) / segments;
+ points += `, ${centerX}px ${centerY}px`;
- for (let i = 0; i <= segments; i++) {
- const angle = ((startAngle + angleStep * i - 90) * Math.PI) / 180;
- const x = centerX + Math.cos(angle) * radius;
- const y = centerY + Math.sin(angle) * radius;
- points += `, ${x}px ${y}px`;
+ return `polygon(${points})`;
}
- points += `, ${centerX}px ${centerY}px`;
-
- return `polygon(${points})`;
- }
+ function positionThemeWheel() {
+ const buttonRect = themeButton.getBoundingClientRect();
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2;
+ const buttonCenterY = buttonRect.top + buttonRect.height / 2;
- function positionThemeWheel() {
- const buttonRect = themeButton.getBoundingClientRect();
- const buttonCenterX = buttonRect.left + buttonRect.width / 2;
- const buttonCenterY = buttonRect.top + buttonRect.height / 2;
+ const margin = 0;
- const margin = 0;
+ let wheelX = buttonCenterX - themeWheelRadius;
+ let wheelY = buttonCenterY - themeWheelRadius;
- let wheelX = buttonCenterX - themeWheelRadius;
- let wheelY = buttonCenterY - themeWheelRadius;
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
+ if (wheelX < margin) {
+ wheelX = margin;
+ } else if (
+ wheelX + themeWheelRadius * 2 >
+ viewportWidth - margin
+ ) {
+ wheelX = viewportWidth - themeWheelRadius * 2 - margin;
+ }
- if (wheelX < margin) {
- wheelX = margin;
- } else if (wheelX + (themeWheelRadius * 2) > viewportWidth - margin) {
- wheelX = viewportWidth - (themeWheelRadius * 2) - margin;
- }
+ if (wheelY < margin) {
+ wheelY = margin;
+ } else if (
+ wheelY + themeWheelRadius * 2 >
+ viewportHeight - margin
+ ) {
+ wheelY = viewportHeight - themeWheelRadius * 2 - margin;
+ }
- if (wheelY < margin) {
- wheelY = margin;
- } else if (wheelY + (themeWheelRadius * 2) > viewportHeight - margin) {
- wheelY = viewportHeight - (themeWheelRadius * 2) - margin;
+ themeWheel.style.left = wheelX + "px";
+ themeWheel.style.top = wheelY + "px";
}
- themeWheel.style.left = wheelX + 'px';
- themeWheel.style.top = wheelY + 'px';
- }
-
- function hideThemeWheel() {
- themeWheel.classList.remove('active');
- }
+ function hideThemeWheel() {
+ themeWheel.classList.remove("active");
+ }
- function showThemeWheel() {
- positionThemeWheel();
- positionThemeIcons();
- themeWheel.classList.add('active');
- }
+ function showThemeWheel() {
+ positionThemeWheel();
+ positionThemeIcons();
+ themeWheel.classList.add("active");
+ }
- function toggleThemeWheel() {
- if (themeWheel.classList.contains('active')) {
- hideThemeWheel();
- } else {
- showThemeWheel();
+ function toggleThemeWheel() {
+ if (themeWheel.classList.contains("active")) {
+ hideThemeWheel();
+ } else {
+ showThemeWheel();
+ }
}
- }
- themeIcons.forEach(icon => {
- icon.addEventListener('click', (e) => {
- e.stopPropagation();
- const theme = icon.getAttribute('data-theme');
- switchTheme(theme);
- themeIcons.forEach(icon => {
- icon.classList.remove('active');
- })
- icon.classList.add('active');
+ themeIcons.forEach((icon) => {
+ icon.addEventListener("click", (e) => {
+ e.stopPropagation();
+ const theme = icon.getAttribute("data-theme");
+ switchTheme(theme);
+ themeIcons.forEach((icon) => {
+ icon.classList.remove("active");
+ });
+ icon.classList.add("active");
+ });
});
- });
-
- themeWheel.addEventListener('mouseleave', hideThemeWheel);
- document.addEventListener('click', (e) => {
- if (!themeWheel.contains(e.target) && e.target !== themeButton && !e.target.classList.contains("toolbar-theme-button-content")) {
- hideThemeWheel();
- }
- });
- </script>
+ themeWheel.addEventListener("mouseleave", hideThemeWheel);
- <script>
- let decorResizeTimeout;
- window.addEventListener('resize', function() {
- clearTimeout(decorResizeTimeout);
- decorResizeTimeout = setTimeout(switchThemeDecor, 300);
- });
- </script>
+ document.addEventListener("click", (e) => {
+ if (
+ !themeWheel.contains(e.target) &&
+ e.target !== themeButton &&
+ !e.target.classList.contains("toolbar-theme-button-content")
+ ) {
+ hideThemeWheel();
+ }
+ });
+ </script>
-</body>
+ <script>
+ let decorResizeTimeout;
+ window.addEventListener("resize", function () {
+ clearTimeout(decorResizeTimeout);
+ decorResizeTimeout = setTimeout(switchThemeDecor, 300);
+ });
+ </script>
+ </body>
</html>
diff --git a/views/pages/global-map.html b/views/pages/global-map.html
index 2459ca1..ce9d5b1 100644
--- a/views/pages/global-map.html
+++ b/views/pages/global-map.html
@@ -1,306 +1,428 @@
-<!DOCTYPE html>
+<!doctype html>
<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="{{ .L.GlobalMap.Description }}">
- <meta property="og:title" content="{{ .L.GlobalMap.Header }}">
- <meta property="og:description" content="{{ .L.GlobalMap.Description }}">
- <meta property="og:url" content="{{ .CanonicalEndpoint }}/{{ .Lang }}/map">
- <meta property="og:type" content="site">
- <title>{{ .L.GlobalMap.Header }} // SAYA TODAY</title>
- <link rel="canonical" href="{{ .CanonicalEndpoint }}/{{ .Lang }}/map">
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/fontawesome.min.css">
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/brands.min.css">
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/solid.min.css">
- {{ block "header" . }}{{ end }}
- <link href="/output.css" rel="stylesheet">
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.css">
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.css">
- <link rel="stylesheet" href="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css">
-</head>
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="description" content="{{ .L.GlobalMap.Description }}" />
+ <meta property="og:title" content="{{ .L.GlobalMap.Header }}" />
+ <meta
+ property="og:description"
+ content="{{ .L.GlobalMap.Description }}"
+ />
+ <meta
+ property="og:url"
+ content="{{ .CanonicalEndpoint }}/{{ .Lang }}/map"
+ />
+ <meta property="og:type" content="site" />
+ <title>{{ .L.GlobalMap.Header }} // SAYA.UZ</title>
+ <link rel="canonical" href="{{ .CanonicalEndpoint }}/{{ .Lang }}/map" />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/fonts/fonts.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/fontawesome.min.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/brands.min.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/solid.min.css"
+ />
+ {{ block "header" . }}{{ end }}
+ <link rel="stylesheet" href="/output.css" />
+ <link
+ rel="icon"
+ href="/favicon-light.svg"
+ type="image/svg+xml"
+ media="(prefers-color-scheme: light)"
+ />
+ <link
+ rel="icon"
+ href="/favicon-dark.svg"
+ type="image/svg+xml"
+ media="(prefers-color-scheme: dark)"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css"
+ />
+ </head>
-<body data-theme="ram" class="font-m-plus text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh">
-
- <script>
- function switchTheme(theme) {
- if (theme == "" || typeof theme == "undefined") {
- const currentTheme = document.body.getAttribute('data-theme');
- switch (currentTheme) {
- case "olive": theme = "lettuce"; break;
- case "lettuce": theme = "night"; break;
- case "night": theme = "ram"; break;
- case "ram": theme = "lilac"; break;
- case "lilac": theme = "olive"; break;
+ <body
+ data-theme="ram"
+ class="font-m-plus text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh"
+ >
+ <script>
+ function switchTheme(theme) {
+ if (theme == "" || typeof theme == "undefined") {
+ const currentTheme =
+ document.body.getAttribute("data-theme");
+ switch (currentTheme) {
+ case "olive":
+ theme = "lettuce";
+ break;
+ case "lettuce":
+ theme = "night";
+ break;
+ case "night":
+ theme = "ram";
+ break;
+ case "ram":
+ theme = "lilac";
+ break;
+ case "lilac":
+ theme = "olive";
+ break;
+ }
}
+ localStorage.setItem("theme", theme);
+ document.body.setAttribute("data-theme", theme);
}
- localStorage.setItem('theme', theme);
- document.body.setAttribute('data-theme', theme);
- }
- const savedTheme = localStorage.getItem('theme') || 'lilac';
- switchTheme(savedTheme);
- </script>
+ const savedTheme = localStorage.getItem("theme") || "lilac";
+ switchTheme(savedTheme);
+ </script>
- <script>
- const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ <script>
+ const clientTimeZone =
+ Intl.DateTimeFormat().resolvedOptions().timeZone;
- function convertRemToPixels(rem) {
- return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
- }
- </script>
+ function convertRemToPixels(rem) {
+ return (
+ rem *
+ parseFloat(
+ getComputedStyle(document.documentElement).fontSize,
+ )
+ );
+ }
+ </script>
- <div id="sidebar" class="bg-sidebar block fixed shadow-[0_0_0.4rem_black] z-1001 w-fit h-auto">
- <div class="text-[3rem] flex flex-col gap-0 relative z-1001">
- <a href="./" class="themed-button themed-button-sidebar w-fit h-fit" draggable="false">
- <i class="fas fa-circle-left flex faw-18 h-18 content-center text-center text-sidebar"></i>
- </a>
- <a href="/{{ .Lang }}" class="themed-button themed-button-sidebar w-fit h-fit" draggable="false">
- <i class="fas fa-home flex faw-18 h-18 content-center text-center text-sidebar"></i>
- </a>
- <a href="/{{ .Lang }}/blog" class="themed-button themed-button-sidebar w-fit h-fit" draggable="false">
- <i class="fas fa-search flex faw-18 h-18 content-center text-center text-sidebar"></i>
- </a>
- <a href="/{{ .Lang }}/map" class="themed-button themed-button-sidebar w-fit h-fit" draggable="false">
- <i class="fas fa-map flex faw-18 h-18 content-center text-center text-sidebar"></i>
- </a>
- <div id="toolbar-theme-button" onclick="toggleThemeWheel();" class="themed-button themed-button-sidebar w-fit h-fit" draggable="false">
- <i id="toolbar-theme-button-content" class="fas fa-swatchbook flex faw-18 h-18 content-center text-center text-sidebar"></i>
+ <div
+ id="sidebar"
+ class="bg-sidebar block fixed shadow-[0_0_0.4rem_black] z-1001 w-fit h-auto"
+ >
+ <div class="text-[3rem] flex flex-col gap-0 relative z-1001">
+ <a
+ href="./"
+ class="themed-button themed-button-sidebar w-fit h-fit"
+ draggable="false"
+ >
+ <i
+ class="fas fa-circle-left flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </a>
+ <a
+ href="/{{ .Lang }}"
+ class="themed-button themed-button-sidebar w-fit h-fit"
+ draggable="false"
+ >
+ <i
+ class="fas fa-home flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </a>
+ <a
+ href="/{{ .Lang }}/blog"
+ class="themed-button themed-button-sidebar w-fit h-fit"
+ draggable="false"
+ >
+ <i
+ class="fas fa-search flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </a>
+ <a
+ href="/{{ .Lang }}/map"
+ class="themed-button themed-button-sidebar w-fit h-fit"
+ draggable="false"
+ >
+ <i
+ class="fas fa-map flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </a>
+ <div
+ id="toolbar-theme-button"
+ onclick="toggleThemeWheel()"
+ class="themed-button themed-button-sidebar w-fit h-fit"
+ draggable="false"
+ >
+ <i
+ id="toolbar-theme-button-content"
+ class="fas fa-swatchbook flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </div>
+ <a
+ href="/{{ .Lang }}/user"
+ id="sidebar-user-button"
+ class="themed-button themed-button-sidebar w-fit h-fit"
+ draggable="false"
+ >
+ <i
+ onclick="return changeUrl('/{{ .Lang }}/user');"
+ class="fas fa-user flex faw-18 h-18 content-center text-center text-sidebar"
+ ></i>
+ </a>
+ </div>
+ <div class="theme-wheel" id="theme-wheel">
+ <div class="theme-icon" data-theme="olive"></div>
+ <div class="theme-icon" data-theme="lettuce"></div>
+ <div class="theme-icon" data-theme="night"></div>
+ <div class="theme-icon" data-theme="ram"></div>
+ <div class="theme-icon" data-theme="lilac"></div>
+ <div class="theme-pin"></div>
</div>
- <a href="/{{ .Lang }}/user" id="sidebar-user-button" class="themed-button themed-button-sidebar w-fit h-fit" draggable="false">
- <i onclick="return changeUrl('/{{ .Lang }}/user');" class="fas fa-user flex faw-18 h-18 content-center text-center text-sidebar"></i>
- </a>
- </div>
- <div class="theme-wheel" id="theme-wheel">
- <div class="theme-icon" data-theme="olive"></div>
- <div class="theme-icon" data-theme="lettuce"></div>
- <div class="theme-icon" data-theme="night"></div>
- <div class="theme-icon" data-theme="ram"></div>
- <div class="theme-icon" data-theme="lilac"></div>
- <div class="theme-pin"></div>
</div>
- </div>
-
- <div id="map-container" class="bg-paper bg-background-dark flex z-10 w-full h-full"></div>
-
- <script src="{{ .StaticStorageBaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script>
-
- <script>
- const themeWheel = document.getElementById('theme-wheel');
- const themeButton = document.getElementById('toolbar-theme-button');
- const themeIcons = document.querySelectorAll('.theme-icon');
- const themeWheelRadius = convertRemToPixels(6);
- const THUMB_BASE = "{{ .ThumbnailBaseUrl }}";
-
- themeIcons.forEach(icon => {
- if (icon.getAttribute('data-theme') == savedTheme) {
- icon.classList.add('active');
- }
- });
- function calculateVmax(percent) {
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
+ <div
+ id="map-container"
+ class="bg-paper bg-background-dark flex z-10 w-full h-full"
+ ></div>
- const largerDimension = Math.max(viewportWidth, viewportHeight);
+ <script src="{{ .StaticStorageBaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script>
+ <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script>
+ <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script>
- const vmaxValue = (largerDimension / 100) * percent;
+ <script>
+ const themeWheel = document.getElementById("theme-wheel");
+ const themeButton = document.getElementById("toolbar-theme-button");
+ const themeIcons = document.querySelectorAll(".theme-icon");
+ const themeWheelRadius = convertRemToPixels(6);
+ const THUMB_BASE = "{{ .ThumbnailBaseUrl }}";
- return vmaxValue;
- }
+ themeIcons.forEach((icon) => {
+ if (icon.getAttribute("data-theme") == savedTheme) {
+ icon.classList.add("active");
+ }
+ });
- function positionThemeIcons() {
- const sliceAngle = 360 / themeIcons.length;
+ function calculateVmax(percent) {
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
- themeIcons.forEach((icon, index) => {
- const startAngle = index * sliceAngle;
- const endAngle = (index + 1) * sliceAngle;
+ const largerDimension = Math.max(viewportWidth, viewportHeight);
- const clipPath = createSliceClipPath(startAngle, endAngle, themeWheelRadius);
- icon.style.clipPath = clipPath;
- });
- }
+ const vmaxValue = (largerDimension / 100) * percent;
- function createSliceClipPath(startAngle, endAngle, radius) {
- const centerX = radius;
- const centerY = radius;
+ return vmaxValue;
+ }
- let points = `${centerX}px ${centerY}px`;
+ function positionThemeIcons() {
+ const sliceAngle = 360 / themeIcons.length;
- const segments = Math.max(2, Math.ceil(Math.abs(endAngle - startAngle) / 5));
- const angleStep = (endAngle - startAngle) / segments;
+ themeIcons.forEach((icon, index) => {
+ const startAngle = index * sliceAngle;
+ const endAngle = (index + 1) * sliceAngle;
- for (let i = 0; i <= segments; i++) {
- const angle = ((startAngle + angleStep * i - 90) * Math.PI) / 180;
- const x = centerX + Math.cos(angle) * radius;
- const y = centerY + Math.sin(angle) * radius;
- points += `, ${x}px ${y}px`;
+ const clipPath = createSliceClipPath(
+ startAngle,
+ endAngle,
+ themeWheelRadius,
+ );
+ icon.style.clipPath = clipPath;
+ });
}
- points += `, ${centerX}px ${centerY}px`;
-
- return `polygon(${points})`;
- }
+ function createSliceClipPath(startAngle, endAngle, radius) {
+ const centerX = radius;
+ const centerY = radius;
- function positionThemeWheel() {
- const buttonRect = themeButton.getBoundingClientRect();
- const buttonCenterX = buttonRect.left + buttonRect.width / 2;
- const buttonCenterY = buttonRect.top + buttonRect.height / 2;
+ let points = `${centerX}px ${centerY}px`;
- const margin = 0;
+ const segments = Math.max(
+ 2,
+ Math.ceil(Math.abs(endAngle - startAngle) / 5),
+ );
+ const angleStep = (endAngle - startAngle) / segments;
- let wheelX = buttonCenterX - themeWheelRadius;
- let wheelY = buttonCenterY - themeWheelRadius;
+ for (let i = 0; i <= segments; i++) {
+ const angle =
+ ((startAngle + angleStep * i - 90) * Math.PI) / 180;
+ const x = centerX + Math.cos(angle) * radius;
+ const y = centerY + Math.sin(angle) * radius;
+ points += `, ${x}px ${y}px`;
+ }
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
+ points += `, ${centerX}px ${centerY}px`;
- if (wheelX < margin) {
- wheelX = margin;
- } else if (wheelX + (themeWheelRadius * 2) > viewportWidth - margin) {
- wheelX = viewportWidth - (themeWheelRadius * 2) - margin;
+ return `polygon(${points})`;
}
- if (wheelY < margin) {
- wheelY = margin;
- } else if (wheelY + (themeWheelRadius * 2) > viewportHeight - margin) {
- wheelY = viewportHeight - (themeWheelRadius * 2) - margin;
- }
+ function positionThemeWheel() {
+ const buttonRect = themeButton.getBoundingClientRect();
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2;
+ const buttonCenterY = buttonRect.top + buttonRect.height / 2;
- themeWheel.style.left = wheelX + 'px';
- themeWheel.style.top = wheelY + 'px';
- }
+ const margin = 0;
- function hideThemeWheel() {
- themeWheel.classList.remove('active');
- }
+ let wheelX = buttonCenterX - themeWheelRadius;
+ let wheelY = buttonCenterY - themeWheelRadius;
- function showThemeWheel() {
- positionThemeWheel();
- positionThemeIcons();
- themeWheel.classList.add('active');
- }
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
- function toggleThemeWheel() {
- if (themeWheel.classList.contains('active')) {
- hideThemeWheel();
- } else {
- showThemeWheel();
- }
- }
+ if (wheelX < margin) {
+ wheelX = margin;
+ } else if (
+ wheelX + themeWheelRadius * 2 >
+ viewportWidth - margin
+ ) {
+ wheelX = viewportWidth - themeWheelRadius * 2 - margin;
+ }
- themeIcons.forEach(icon => {
- icon.addEventListener('click', (e) => {
- e.stopPropagation();
- const theme = icon.getAttribute('data-theme');
- switchTheme(theme);
- themeIcons.forEach(icon => {
- icon.classList.remove('active');
- })
- icon.classList.add('active');
- });
- });
+ if (wheelY < margin) {
+ wheelY = margin;
+ } else if (
+ wheelY + themeWheelRadius * 2 >
+ viewportHeight - margin
+ ) {
+ wheelY = viewportHeight - themeWheelRadius * 2 - margin;
+ }
- themeWheel.addEventListener('mouseleave', hideThemeWheel);
+ themeWheel.style.left = wheelX + "px";
+ themeWheel.style.top = wheelY + "px";
+ }
- document.addEventListener('click', (e) => {
- if (!themeWheel.contains(e.target) && e.target !== themeButton && !e.target.classList.contains("toolbar-theme-button-content")) {
- hideThemeWheel();
+ function hideThemeWheel() {
+ themeWheel.classList.remove("active");
}
- });
- </script>
- <script>
- var map;
- var markers = L.markerClusterGroup();
+ function showThemeWheel() {
+ positionThemeWheel();
+ positionThemeIcons();
+ themeWheel.classList.add("active");
+ }
- function createCustomIcon(color, borderColor = "var(--color-main-soft)") {
- const svgIcon = `
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
- <path d="M12 1C7.5 1 4 4.5 4 9c0 6.5 8 14 8 14s8-7.5 8-14c0-4.5-3.5-8-8-8zm0 11c-1.5 0-3-1.5-3-3s1.5-3 3-3 3 1.5 3 3-1.5 3-3 3z"
- fill="${color}"
- stroke="${borderColor}"
- stroke-width="1"
- stroke-linejoin="round"/>
- </svg>
- `;
+ function toggleThemeWheel() {
+ if (themeWheel.classList.contains("active")) {
+ hideThemeWheel();
+ } else {
+ showThemeWheel();
+ }
+ }
- return L.divIcon({
- html: svgIcon,
- className: 'custom-svg-marker',
- iconSize: [32, 32],
- iconAnchor: [16, 32],
- popupAnchor: [0, -32]
+ themeIcons.forEach((icon) => {
+ icon.addEventListener("click", (e) => {
+ e.stopPropagation();
+ const theme = icon.getAttribute("data-theme");
+ switchTheme(theme);
+ themeIcons.forEach((icon) => {
+ icon.classList.remove("active");
+ });
+ icon.classList.add("active");
+ });
});
- }
- function initLocationMap() {
- map = L.map('map-container').setView([{{ .MapLocationLat }}, {{ .MapLocationLong }}], 4);
+ themeWheel.addEventListener("mouseleave", hideThemeWheel);
- L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', {
- maxZoom: 20,
- attribution: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
- zoomControl: false
- }).addTo(map);
+ document.addEventListener("click", (e) => {
+ if (
+ !themeWheel.contains(e.target) &&
+ e.target !== themeButton &&
+ !e.target.classList.contains("toolbar-theme-button-content")
+ ) {
+ hideThemeWheel();
+ }
+ });
+ </script>
- L.control.zoom({
- position: 'topright'
- }).addTo(map);
+ <script>
+ var map;
+ var markers = L.markerClusterGroup();
- map.addLayer(markers);
- }
+ function createCustomIcon(color, borderColor = "var(--color-main-soft)") {
+ const svgIcon = `
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12 1C7.5 1 4 4.5 4 9c0 6.5 8 14 8 14s8-7.5 8-14c0-4.5-3.5-8-8-8zm0 11c-1.5 0-3-1.5-3-3s1.5-3 3-3 3 1.5 3 3-1.5 3-3 3z"
+ fill="${color}"
+ stroke="${borderColor}"
+ stroke-width="1"
+ stroke-linejoin="round"/>
+ </svg>
+ `;
- function markerToHsl(str, newCoef) {
- let hash = 0;
- for (let i = 0; i < str.length / 2; i++) {
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
- hash = str.charCodeAt(str.length - i - 1) + ((hash << 5) - hash);
- hash %= 360;
+ return L.divIcon({
+ html: svgIcon,
+ className: 'custom-svg-marker',
+ iconSize: [32, 32],
+ iconAnchor: [16, 32],
+ popupAnchor: [0, -32]
+ });
}
- return [hash, (20 + newCoef * 80).toFixed(0), (20 + newCoef * 75).toFixed(0)];
- }
+ function initLocationMap() {
+ map = L.map('map-container').setView([{{ .MapLocationLat }}, {{ .MapLocationLong }}], 4);
- function mapAddMarker(x, y, relativeErrorMeters = 0, title = "", link = "", thumbnail = "", newCoef = 1) {
- const [h, s, l] = markerToHsl(title, newCoef);
- const color = `hsl(${h} ${s}% ${l}%)`;
+ L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', {
+ maxZoom: 20,
+ attribution: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
+ zoomControl: false
+ }).addTo(map);
- if (relativeErrorMeters != 0) {
- var circle = L.circle([x, y], {
- color: color,
- fillColor: color,
- fillOpacity: 0.15,
- radius: relativeErrorMeters
+ L.control.zoom({
+ position: 'topright'
}).addTo(map);
+
+ map.addLayer(markers);
}
- var marker = L.marker([x, y], {
- icon: createCustomIcon(color),
- title: title,
- }).bindPopup(`
- <div class="flex flex-row justify-center justify-items-center">
- <img onclick="location.href='${link}';" class="cursor-pointer object-cover rounded-[10%] select-none w-12 h-12 mr-1" src="${THUMB_BASE.replace('%s', thumbnail)}">
- <span><b><a href="${link}">${title}</a></b><br><a href="https://www.openstreetmap.org/#map=13/${x}/${y}">${x}, ${y}</a></span>
- </div>`);
+ function markerToHsl(str, newCoef) {
+ let hash = 0;
+ for (let i = 0; i < str.length / 2; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ hash = str.charCodeAt(str.length - i - 1) + ((hash << 5) - hash);
+ hash %= 360;
+ }
- markers.addLayer(marker);
- }
+ return [hash, (20 + newCoef * 80).toFixed(0), (20 + newCoef * 75).toFixed(0)];
+ }
- document.addEventListener('DOMContentLoaded', () => {
- initLocationMap();
- {{- range .MapMarkers }}
- mapAddMarker({{ .Lat }}, {{ .Long }}, {{ .AccuracyMeters }}, {{ .Title }}, {{ .PageLink }}, {{ .Thumbnail }}, {{ fdiv .Index (len $.MapMarkers) }});
- {{- end }}
- });
+ function mapAddMarker(x, y, relativeErrorMeters = 0, title = "", link = "", thumbnail = "", newCoef = 1) {
+ const [h, s, l] = markerToHsl(title, newCoef);
+ const color = `hsl(${h} ${s}% ${l}%)`;
- window.addEventListener('resize', function() {
- setTimeout(() => {
- map.invalidateSize();
- }, 100);
- });
- </script>
+ if (relativeErrorMeters != 0) {
+ var circle = L.circle([x, y], {
+ color: color,
+ fillColor: color,
+ fillOpacity: 0.15,
+ radius: relativeErrorMeters
+ }).addTo(map);
+ }
-</body>
+ var marker = L.marker([x, y], {
+ icon: createCustomIcon(color),
+ title: title,
+ }).bindPopup(`
+ <div class="flex flex-row justify-center justify-items-center">
+ <img onclick="location.href='${link}';" class="cursor-pointer object-cover rounded-[10%] select-none w-12 h-12 mr-1" src="${THUMB_BASE.replace('%s', thumbnail)}">
+ <span><b><a href="${link}">${title}</a></b><br><a href="https://www.openstreetmap.org/#map=13/${x}/${y}">${x}, ${y}</a></span>
+ </div>`);
+
+ markers.addLayer(marker);
+ }
+
+ document.addEventListener('DOMContentLoaded', () => {
+ initLocationMap();
+ {{- range .MapMarkers }}
+ mapAddMarker({{ .Lat }}, {{ .Long }}, {{ .AccuracyMeters }}, {{ .Title }}, {{ .PageLink }}, {{ .Thumbnail }}, {{ fdiv .Index (len $.MapMarkers) }});
+ {{- end }}
+ });
+
+ window.addEventListener('resize', function() {
+ setTimeout(() => {
+ map.invalidateSize();
+ }, 100);
+ });
+ </script>
+ </body>
</html>
diff --git a/views/pages/robots.txt b/views/pages/robots.txt
index 1e5bebb..aaef3f4 100644
--- a/views/pages/robots.txt
+++ b/views/pages/robots.txt
@@ -1,5 +1,7 @@
User-agent: *
Allow: /
-Disallow: /api/
+Disallow: /api/v1/email/
+Disallow: /ru/user/
+Disallow: /en/user/
Sitemap: {{ .CanonicalEndpoint }}/sitemap.xml \ No newline at end of file
diff --git a/views/pages/unsubscribe-page.html b/views/pages/unsubscribe-page.html
index bb5d64b..a917746 100644
--- a/views/pages/unsubscribe-page.html
+++ b/views/pages/unsubscribe-page.html
@@ -1,34 +1,77 @@
-<!DOCTYPE html>
+<!doctype html>
<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="robots" content="noindex,nofollow">
- <title>{{ .L.UnsubscribePage.Header }} // SAYA TODAY</title>
-<style>
- .bg-paper {
- background-image: url(data:image/webp;base64,UklGRi4BAABXRUJQVlA4ICIBAABQCQCdASqAAIAAPm02lUmkIqIkIImIgA2JaW3rMFmGDAOKLAoJtP591ByFR74Tun2DP+Lygpfsz5FH7+cKgxi2Islfd03i5AJdXUfU0SDc8SXYAAD++hlzeYAJWQEF8ZdkJdlcA+5btZg0FMGaso8SHoBph0qAOQysElJ5C5SJFNoKIV1J550UyL7Y9Gh0/c/uzLnBmr8lvz7k1WeA81H4S5ViU/wBqa7aM1m8D9rDtrk8cSTqx+xWF6MS8UfZGSsks6CT/igtgv6syFN9uNA3LjnjiCJKYOvFcMkX+jB2578GWVo3nyLaM1QNPYCxn3TiUbnf7U0MdD2NsBpg1+Jd1wu6yW/U+goN+IQS6Nt4RMWltVcZHlxrneKJi0SobO0AAA==);
- background-size: 128px;
- background-repeat: repeat;
- background-blend-mode: multiply;
- }
-</style>
-</head>
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="robots" content="noindex,nofollow" />
+ <link
+ rel="icon"
+ href="/favicon-light.svg"
+ type="image/svg+xml"
+ media="(prefers-color-scheme: light)"
+ />
+ <link
+ rel="icon"
+ href="/favicon-dark.svg"
+ type="image/svg+xml"
+ media="(prefers-color-scheme: dark)"
+ />
+ <title>{{ .L.UnsubscribePage.Header }} // SAYA.UZ</title>
+ <style>
+ .bg-paper {
+ background-image: url(data:image/webp;base64,UklGRi4BAABXRUJQVlA4ICIBAABQCQCdASqAAIAAPm02lUmkIqIkIImIgA2JaW3rMFmGDAOKLAoJtP591ByFR74Tun2DP+Lygpfsz5FH7+cKgxi2Islfd03i5AJdXUfU0SDc8SXYAAD++hlzeYAJWQEF8ZdkJdlcA+5btZg0FMGaso8SHoBph0qAOQysElJ5C5SJFNoKIV1J550UyL7Y9Gh0/c/uzLnBmr8lvz7k1WeA81H4S5ViU/wBqa7aM1m8D9rDtrk8cSTqx+xWF6MS8UfZGSsks6CT/igtgv6syFN9uNA3LjnjiCJKYOvFcMkX+jB2578GWVo3nyLaM1QNPYCxn3TiUbnf7U0MdD2NsBpg1+Jd1wu6yW/U+goN+IQS6Nt4RMWltVcZHlxrneKJi0SobO0AAA==);
+ background-size: 128px;
+ background-repeat: repeat;
+ background-blend-mode: multiply;
+ }
+ </style>
+ </head>
-<body style="width: 100dvw; height: 100dvh; display: flex; flex-direction: column; position: fixed">
+ <body
+ style="
+ width: 100dvw;
+ height: 100dvh;
+ display: flex;
+ flex-direction: column;
+ position: fixed;
+ "
+ >
+ <div
+ class="bg-paper"
+ style="flex: 3 0 0; background-color: rgba({{ .StatusColor }}, 0.1)"
+ ></div>
- <div class="bg-paper" style='flex: 3 0 0; background-color: rgba({{ .StatusColor }}, 0.1)'></div>
+ <div class="bg-paper" style="flex: 1 0 0; align-content: center">
+ <p
+ style="
+ position: relative;
+ width: 100%;
+ margin-inline: auto;
+ text-align: center;
+ font-size: 2rem;
+ "
+ >
+ {{ .StatusEmoji }}
+ </p>
+ </div>
- <div class="bg-paper" style="flex: 1 0 0; align-content: center;">
- <p style="position: relative; width: 100%; margin-inline: auto; text-align: center; font-size: 2rem;">{{ .StatusEmoji }}</p>
- </div>
-
- <div class="bg-paper" style="flex: 1 0 0; align-content: center;">
- <p style="position: relative; width: 100%; margin-inline: auto; text-align: center; font-size: 1.5rem;">{{ .StatusText }}</p>
- </div>
-
- <div class="bg-paper" style='flex: 3 0 0; background-color: rgba({{ .StatusColor }}, 0.2)'></div>
-
-</body>
+ <div class="bg-paper" style="flex: 1 0 0; align-content: center">
+ <p
+ style="
+ position: relative;
+ width: 100%;
+ margin-inline: auto;
+ text-align: center;
+ font-size: 1.5rem;
+ "
+ >
+ {{ .StatusText }}
+ </p>
+ </div>
+ <div
+ class="bg-paper"
+ style="flex: 3 0 0; background-color: rgba({{ .StatusColor }}, 0.2)"
+ ></div>
+ </body>
</html>
diff --git a/views/partials/general-page-footer.html b/views/partials/general-page-footer.html
index 3001d5d..3f5f9ee 100644
--- a/views/partials/general-page-footer.html
+++ b/views/partials/general-page-footer.html
@@ -1,7 +1,23 @@
{{ block "footer" . }}{{ end }}
-<p class="grow content-center text-base/[1] font-gentium font-thin italic text-right text-secondary mr-1">
- All the photos on <a href="https://saya.today/">saya.today</a> are licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> by <a href="https://t.me/EarlInisMona">Saya Andy</a> © {{ .PublishedYear }}
+<p
+ class="grow content-center text-base/[1] font-gentium font-thin italic text-right text-secondary mr-1"
+>
+ All the photos on <a href="https://saya.uz/">saya.uz</a> are licensed under
+ <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>
+ by <a href="https://t.me/EarlInisMona">Saya Andy</a> © {{ .PublishedYear }}
</p>
-<img class="max-w-4 max-h-4 self-center" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="CC">
-<img class="max-w-4 max-h-4 self-center" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="BY">
-<img class="max-w-4 max-h-4 self-center" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="SA"> \ No newline at end of file
+<img
+ class="max-w-4 max-h-4 self-center"
+ src="https://mirrors.creativecommons.org/presskit/icons/cc.svg"
+ alt="CC"
+/>
+<img
+ class="max-w-4 max-h-4 self-center"
+ src="https://mirrors.creativecommons.org/presskit/icons/by.svg"
+ alt="BY"
+/>
+<img
+ class="max-w-4 max-h-4 self-center"
+ src="https://mirrors.creativecommons.org/presskit/icons/sa.svg"
+ alt="SA"
+/>
diff --git a/views/partials/general-page-header.html b/views/partials/general-page-header.html
index ad11ffb..995ebf3 100644
--- a/views/partials/general-page-header.html
+++ b/views/partials/general-page-header.html
@@ -1,5 +1,9 @@
-<title>{{ .Title }} // SAYA TODAY</title>
+<title>{{ .Title }} // SAYA.UZ</title>
<div class="flex flex-row mb-2">
{{ block "header" . }}{{ end }}
- <p class="text-4xl font-m-plus font-bold italic text-shadow-[0_2px_2px_var(--color-main-soft)] text-left my-auto">{{ .Title }}</p>
-</div> \ No newline at end of file
+ <p
+ class="text-4xl font-m-plus font-bold italic text-shadow-[0_2px_2px_var(--color-main-soft)] text-left my-auto"
+ >
+ {{ .Title }}
+ </p>
+</div>